Merge pull request #778 from square/z/defaultValue

Add support for default values in PolymorphicJsonAdapterFactory
This commit is contained in:
Jesse Wilson
2018-12-31 22:04:07 -05:00
committed by GitHub
2 changed files with 88 additions and 7 deletions

View File

@@ -29,6 +29,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import javax.annotation.CheckReturnValue; import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
/** /**
* A JsonAdapter factory for objects that include type information in the JSON. When decoding JSON * A JsonAdapter factory for objects that include type information in the JSON. When decoding JSON
@@ -100,19 +101,31 @@ import javax.annotation.CheckReturnValue;
* <p>If an unknown subtype is encountered when decoding, this will throw a {@link * <p>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 * JsonDataException}. If an unknown type is encountered when encoding, this will throw an {@link
* IllegalArgumentException}. * IllegalArgumentException}.
*
* <p>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<T> implements JsonAdapter.Factory { public final class PolymorphicJsonAdapterFactory<T> implements JsonAdapter.Factory {
final Class<T> baseType; final Class<T> baseType;
final String labelKey; final String labelKey;
final List<String> labels; final List<String> labels;
final List<Type> subtypes; final List<Type> subtypes;
@Nullable final T defaultValue;
final boolean defaultValueSet;
PolymorphicJsonAdapterFactory( PolymorphicJsonAdapterFactory(
Class<T> baseType, String labelKey, List<String> labels, List<Type> subtypes) { Class<T> baseType,
String labelKey,
List<String> labels,
List<Type> subtypes,
@Nullable T defaultValue,
boolean defaultValueSet) {
this.baseType = baseType; this.baseType = baseType;
this.labelKey = labelKey; this.labelKey = labelKey;
this.labels = labels; this.labels = labels;
this.subtypes = subtypes; this.subtypes = subtypes;
this.defaultValue = defaultValue;
this.defaultValueSet = defaultValueSet;
} }
/** /**
@@ -125,7 +138,12 @@ public final class PolymorphicJsonAdapterFactory<T> implements JsonAdapter.Facto
if (baseType == null) throw new NullPointerException("baseType == null"); if (baseType == null) throw new NullPointerException("baseType == null");
if (labelKey == null) throw new NullPointerException("labelKey == null"); if (labelKey == null) throw new NullPointerException("labelKey == null");
return new PolymorphicJsonAdapterFactory<>( return new PolymorphicJsonAdapterFactory<>(
baseType, labelKey, Collections.<String>emptyList(), Collections.<Type>emptyList()); baseType,
labelKey,
Collections.<String>emptyList(),
Collections.<Type>emptyList(),
null,
false);
} }
/** /**
@@ -143,7 +161,25 @@ public final class PolymorphicJsonAdapterFactory<T> implements JsonAdapter.Facto
newLabels.add(label); newLabels.add(label);
List<Type> newSubtypes = new ArrayList<>(subtypes); List<Type> newSubtypes = new ArrayList<>(subtypes);
newSubtypes.add(subtype); 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<T> withDefaultValue(@Nullable T defaultValue) {
return new PolymorphicJsonAdapterFactory<>(baseType,
labelKey,
labels,
subtypes,
defaultValue,
true);
} }
@Override @Override
@@ -157,7 +193,13 @@ public final class PolymorphicJsonAdapterFactory<T> implements JsonAdapter.Facto
jsonAdapters.add(moshi.adapter(subtypes.get(i))); 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<Object> { static final class PolymorphicJsonAdapter extends JsonAdapter<Object> {
@@ -165,18 +207,26 @@ public final class PolymorphicJsonAdapterFactory<T> implements JsonAdapter.Facto
final List<String> labels; final List<String> labels;
final List<Type> subtypes; final List<Type> subtypes;
final List<JsonAdapter<Object>> jsonAdapters; final List<JsonAdapter<Object>> jsonAdapters;
@Nullable final Object defaultValue;
final boolean defaultValueSet;
/** Single-element options containing the label's key only. */ /** Single-element options containing the label's key only. */
final JsonReader.Options labelKeyOptions; final JsonReader.Options labelKeyOptions;
/** Corresponds to subtypes. */ /** Corresponds to subtypes. */
final JsonReader.Options labelOptions; final JsonReader.Options labelOptions;
PolymorphicJsonAdapter(String labelKey, List<String> labels, PolymorphicJsonAdapter(String labelKey,
List<Type> subtypes, List<JsonAdapter<Object>> jsonAdapters) { List<String> labels,
List<Type> subtypes,
List<JsonAdapter<Object>> jsonAdapters,
@Nullable Object defaultValue,
boolean defaultValueSet) {
this.labelKey = labelKey; this.labelKey = labelKey;
this.labels = labels; this.labels = labels;
this.subtypes = subtypes; this.subtypes = subtypes;
this.jsonAdapters = jsonAdapters; this.jsonAdapters = jsonAdapters;
this.defaultValue = defaultValue;
this.defaultValueSet = defaultValueSet;
this.labelKeyOptions = JsonReader.Options.of(labelKey); this.labelKeyOptions = JsonReader.Options.of(labelKey);
this.labelOptions = JsonReader.Options.of(labels.toArray(new String[0])); this.labelOptions = JsonReader.Options.of(labels.toArray(new String[0]));
@@ -184,6 +234,10 @@ public final class PolymorphicJsonAdapterFactory<T> implements JsonAdapter.Facto
@Override public Object fromJson(JsonReader reader) throws IOException { @Override public Object fromJson(JsonReader reader) throws IOException {
int labelIndex = labelIndex(reader.peekJson()); int labelIndex = labelIndex(reader.peekJson());
if (labelIndex == -1) {
reader.skipValue();
return defaultValue;
}
return jsonAdapters.get(labelIndex).fromJson(reader); return jsonAdapters.get(labelIndex).fromJson(reader);
} }
@@ -197,7 +251,7 @@ public final class PolymorphicJsonAdapterFactory<T> implements JsonAdapter.Facto
} }
int labelIndex = reader.selectString(labelOptions); int labelIndex = reader.selectString(labelOptions);
if (labelIndex == -1) { if (labelIndex == -1 && !defaultValueSet) {
throw new JsonDataException("Expected one of " throw new JsonDataException("Expected one of "
+ labels + labels
+ " for key '" + " for key '"

View File

@@ -78,6 +78,33 @@ public final class PolymorphicJsonAdapterFactoryTest {
assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_OBJECT); assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_OBJECT);
} }
@Test public void specifiedFallbackSubtype() throws IOException {
Error fallbackError = new Error(Collections.<String, Object>emptyMap());
Moshi moshi = new Moshi.Builder()
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success")
.withSubtype(Error.class, "error")
.withDefaultValue(fallbackError))
.build();
JsonAdapter<Message> 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<Message> adapter = moshi.adapter(Message.class);
Message message = adapter.fromJson("{\"type\":\"data\",\"value\":\"Okay!\"}");
assertThat(message).isNull();
}
@Test public void unregisteredSubtype() { @Test public void unregisteredSubtype() {
Moshi moshi = new Moshi.Builder() Moshi moshi = new Moshi.Builder()
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type") .add(PolymorphicJsonAdapterFactory.of(Message.class, "type")