diff --git a/adapters/src/main/java/com/squareup/moshi/adapters/EnumJsonAdapter.java b/adapters/src/main/java/com/squareup/moshi/adapters/EnumJsonAdapter.java new file mode 100644 index 0000000..bc06baf --- /dev/null +++ b/adapters/src/main/java/com/squareup/moshi/adapters/EnumJsonAdapter.java @@ -0,0 +1,88 @@ +package com.squareup.moshi.adapters; + +import com.squareup.moshi.Json; +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.JsonDataException; +import com.squareup.moshi.JsonReader; +import com.squareup.moshi.JsonWriter; +import java.io.IOException; +import java.util.Arrays; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * A JsonAdapter for enums that allows having a fallback enum value when a deserialized string does + * not match any enum value. + */ +public final class EnumJsonAdapter> extends JsonAdapter { + final Class enumType; + final String[] nameStrings; + final T[] constants; + final JsonReader.Options options; + final @Nullable T fallbackValue; + + public static > EnumJsonAdapter create(Class enumType) { + return new EnumJsonAdapter<>(enumType, null); + } + + /** + * Create a new adapter for this enum with a fallback value to use when the JSON string does not + * match any of the enum's constants. Note that this value will not be used when the JSON value is + * null, absent, or not a string. Also, the string values are case-sensitive, and this fallback + * value will be used even on case mismatches. + */ + public EnumJsonAdapter withUnknownFallback(T fallbackValue) { + if (fallbackValue == null) { + throw new NullPointerException("fallbackValue == null"); + } + return new EnumJsonAdapter<>(enumType, fallbackValue); + } + + EnumJsonAdapter(Class enumType, @Nullable T fallbackValue) { + this.enumType = enumType; + this.fallbackValue = fallbackValue; + try { + constants = enumType.getEnumConstants(); + nameStrings = new String[constants.length]; + for (int i = 0; i < constants.length; i++) { + String constantName = constants[i].name(); + Json annotation = enumType.getField(constantName).getAnnotation(Json.class); + String name = annotation != null ? annotation.name() : constantName; + nameStrings[i] = name; + } + options = JsonReader.Options.of(nameStrings); + } catch (NoSuchFieldException e) { + throw new AssertionError("Missing field in " + enumType.getName(), e); + } + } + + @Override public @Nonnull T fromJson(JsonReader reader) throws IOException { + int index = reader.selectString(options); + if (index != -1) return constants[index]; + + String path = reader.getPath(); + if (fallbackValue == null) { + String name = reader.nextString(); + throw new JsonDataException("Expected one of " + + Arrays.asList(nameStrings) + " but was " + name + " at path " + path); + } + if (reader.peek() != JsonReader.Token.STRING) { + throw new JsonDataException( + "Expected a string but was " + reader.peek() + " at path " + path); + } + reader.skipValue(); + return fallbackValue; + } + + @Override public void toJson(JsonWriter writer, T value) throws IOException { + if (value == null) { + throw new NullPointerException( + "value was null! Wrap in .nullSafe() to write nullable values."); + } + writer.value(nameStrings[value.ordinal()]); + } + + @Override public String toString() { + return "EnumJsonAdapter(" + enumType.getName() + ")"; + } +} diff --git a/adapters/src/test/java/com/squareup/moshi/adapters/EnumJsonAdapterTest.java b/adapters/src/test/java/com/squareup/moshi/adapters/EnumJsonAdapterTest.java new file mode 100644 index 0000000..09c7816 --- /dev/null +++ b/adapters/src/test/java/com/squareup/moshi/adapters/EnumJsonAdapterTest.java @@ -0,0 +1,52 @@ +package com.squareup.moshi.adapters; + +import com.squareup.moshi.Json; +import com.squareup.moshi.JsonDataException; +import com.squareup.moshi.JsonReader; +import okio.Buffer; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; + +@SuppressWarnings("CheckReturnValue") +public final class EnumJsonAdapterTest { + @Test public void toAndFromJson() throws Exception { + EnumJsonAdapter adapter = EnumJsonAdapter.create(Roshambo.class); + assertThat(adapter.fromJson("\"ROCK\"")).isEqualTo(Roshambo.ROCK); + assertThat(adapter.toJson(Roshambo.PAPER)).isEqualTo("\"PAPER\""); + } + + @Test public void withJsonName() throws Exception { + EnumJsonAdapter adapter = EnumJsonAdapter.create(Roshambo.class); + assertThat(adapter.fromJson("\"scr\"")).isEqualTo(Roshambo.SCISSORS); + assertThat(adapter.toJson(Roshambo.SCISSORS)).isEqualTo("\"scr\""); + } + + @Test public void withoutFallbackValue() throws Exception { + EnumJsonAdapter adapter = EnumJsonAdapter.create(Roshambo.class); + JsonReader reader = JsonReader.of(new Buffer().writeUtf8("\"SPOCK\"")); + try { + adapter.fromJson(reader); + fail(); + } catch (JsonDataException expected) { + assertThat(expected).hasMessage( + "Expected one of [ROCK, PAPER, scr] but was SPOCK at path $"); + } + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + @Test public void withFallbackValue() throws Exception { + EnumJsonAdapter adapter = EnumJsonAdapter.create(Roshambo.class) + .withUnknownFallback(Roshambo.ROCK); + JsonReader reader = JsonReader.of(new Buffer().writeUtf8("\"SPOCK\"")); + assertThat(adapter.fromJson(reader)).isEqualTo(Roshambo.ROCK); + assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); + } + + enum Roshambo { + ROCK, + PAPER, + @Json(name = "scr") SCISSORS + } +}