From fead71bca0520422374a7b7c73f9488bad36207a Mon Sep 17 00:00:00 2001 From: Zac Sweers Date: Mon, 31 Dec 2018 16:21:57 -0800 Subject: [PATCH] Add support for default values in PolymorphicJsonAdapterFactory Picking from #741 Resolves #739 --- .../PolymorphicJsonAdapterFactory.java | 68 +++++++++++++++++-- .../PolymorphicJsonAdapterFactoryTest.java | 27 ++++++++ 2 files changed, 88 insertions(+), 7 deletions(-) diff --git a/adapters/src/main/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactory.java b/adapters/src/main/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactory.java index fec728c..02023b7 100644 --- a/adapters/src/main/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactory.java +++ b/adapters/src/main/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactory.java @@ -29,6 +29,7 @@ import java.util.Collections; import java.util.List; import java.util.Set; import javax.annotation.CheckReturnValue; +import javax.annotation.Nullable; /** * A JsonAdapter factory for objects that include type information in the JSON. When decoding JSON @@ -100,19 +101,31 @@ import javax.annotation.CheckReturnValue; *

If an unknown subtype is encountered when decoding, this will throw a {@link * JsonDataException}. If an unknown type is encountered when encoding, this will throw an {@link * IllegalArgumentException}. + * + *

If you want to specify a custom unknown fallback for decoding, you can do so via + * {@link #withDefaultValue(Object)}. This instance should be immutable, as it is shared. */ public final class PolymorphicJsonAdapterFactory implements JsonAdapter.Factory { final Class baseType; final String labelKey; final List labels; final List subtypes; + @Nullable final T defaultValue; + final boolean defaultValueSet; PolymorphicJsonAdapterFactory( - Class baseType, String labelKey, List labels, List subtypes) { + Class baseType, + String labelKey, + List labels, + List subtypes, + @Nullable T defaultValue, + boolean defaultValueSet) { this.baseType = baseType; this.labelKey = labelKey; this.labels = labels; this.subtypes = subtypes; + this.defaultValue = defaultValue; + this.defaultValueSet = defaultValueSet; } /** @@ -125,7 +138,12 @@ public final class PolymorphicJsonAdapterFactory implements JsonAdapter.Facto if (baseType == null) throw new NullPointerException("baseType == null"); if (labelKey == null) throw new NullPointerException("labelKey == null"); return new PolymorphicJsonAdapterFactory<>( - baseType, labelKey, Collections.emptyList(), Collections.emptyList()); + baseType, + labelKey, + Collections.emptyList(), + Collections.emptyList(), + null, + false); } /** @@ -143,7 +161,25 @@ public final class PolymorphicJsonAdapterFactory implements JsonAdapter.Facto newLabels.add(label); List newSubtypes = new ArrayList<>(subtypes); newSubtypes.add(subtype); - return new PolymorphicJsonAdapterFactory<>(baseType, labelKey, newLabels, newSubtypes); + return new PolymorphicJsonAdapterFactory<>(baseType, + labelKey, + newLabels, + newSubtypes, + defaultValue, + defaultValueSet); + } + + /** + * Returns a new factory that with default to {@code defaultValue} upon decoding of unrecognized + * labels. The default value should be immutable. + */ + public PolymorphicJsonAdapterFactory withDefaultValue(@Nullable T defaultValue) { + return new PolymorphicJsonAdapterFactory<>(baseType, + labelKey, + labels, + subtypes, + defaultValue, + true); } @Override @@ -157,7 +193,13 @@ public final class PolymorphicJsonAdapterFactory implements JsonAdapter.Facto jsonAdapters.add(moshi.adapter(subtypes.get(i))); } - return new PolymorphicJsonAdapter(labelKey, labels, subtypes, jsonAdapters).nullSafe(); + return new PolymorphicJsonAdapter(labelKey, + labels, + subtypes, + jsonAdapters, + defaultValue, + defaultValueSet + ).nullSafe(); } static final class PolymorphicJsonAdapter extends JsonAdapter { @@ -165,18 +207,26 @@ public final class PolymorphicJsonAdapterFactory implements JsonAdapter.Facto final List labels; final List subtypes; final List> jsonAdapters; + @Nullable final Object defaultValue; + final boolean defaultValueSet; /** Single-element options containing the label's key only. */ final JsonReader.Options labelKeyOptions; /** Corresponds to subtypes. */ final JsonReader.Options labelOptions; - PolymorphicJsonAdapter(String labelKey, List labels, - List subtypes, List> jsonAdapters) { + PolymorphicJsonAdapter(String labelKey, + List labels, + List subtypes, + List> jsonAdapters, + @Nullable Object defaultValue, + boolean defaultValueSet) { this.labelKey = labelKey; this.labels = labels; this.subtypes = subtypes; this.jsonAdapters = jsonAdapters; + this.defaultValue = defaultValue; + this.defaultValueSet = defaultValueSet; this.labelKeyOptions = JsonReader.Options.of(labelKey); this.labelOptions = JsonReader.Options.of(labels.toArray(new String[0])); @@ -184,6 +234,10 @@ public final class PolymorphicJsonAdapterFactory implements JsonAdapter.Facto @Override public Object fromJson(JsonReader reader) throws IOException { int labelIndex = labelIndex(reader.peekJson()); + if (labelIndex == -1) { + reader.skipValue(); + return defaultValue; + } return jsonAdapters.get(labelIndex).fromJson(reader); } @@ -197,7 +251,7 @@ public final class PolymorphicJsonAdapterFactory implements JsonAdapter.Facto } int labelIndex = reader.selectString(labelOptions); - if (labelIndex == -1) { + if (labelIndex == -1 && !defaultValueSet) { throw new JsonDataException("Expected one of " + labels + " for key '" diff --git a/adapters/src/test/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactoryTest.java b/adapters/src/test/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactoryTest.java index e4a79a3..816ba66 100644 --- a/adapters/src/test/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactoryTest.java +++ b/adapters/src/test/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactoryTest.java @@ -78,6 +78,33 @@ public final class PolymorphicJsonAdapterFactoryTest { assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_OBJECT); } + @Test public void specifiedFallbackSubtype() throws IOException { + Error fallbackError = new Error(Collections.emptyMap()); + Moshi moshi = new Moshi.Builder() + .add(PolymorphicJsonAdapterFactory.of(Message.class, "type") + .withSubtype(Success.class, "success") + .withSubtype(Error.class, "error") + .withDefaultValue(fallbackError)) + .build(); + JsonAdapter adapter = moshi.adapter(Message.class); + + Message message = adapter.fromJson("{\"type\":\"data\",\"value\":\"Okay!\"}"); + assertThat(message).isSameAs(fallbackError); + } + + @Test public void specifiedNullFallbackSubtype() throws IOException { + Moshi moshi = new Moshi.Builder() + .add(PolymorphicJsonAdapterFactory.of(Message.class, "type") + .withSubtype(Success.class, "success") + .withSubtype(Error.class, "error") + .withDefaultValue(null)) + .build(); + JsonAdapter adapter = moshi.adapter(Message.class); + + Message message = adapter.fromJson("{\"type\":\"data\",\"value\":\"Okay!\"}"); + assertThat(message).isNull(); + } + @Test public void unregisteredSubtype() { Moshi moshi = new Moshi.Builder() .add(PolymorphicJsonAdapterFactory.of(Message.class, "type")