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 1019a8c..16b819b 100644
--- a/adapters/src/main/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactory.java
+++ b/adapters/src/main/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactory.java
@@ -95,38 +95,52 @@ import javax.annotation.Nullable;
*
Each type identifier must be unique.
*
*
- * For best performance type information should be the first field in the object. Otherwise Moshi
- * must reprocess the JSON stream once it knows the object's type.
+ *
For best performance type information should be the first field in the object. Otherwise
+ * Moshi must reprocess the JSON stream once it knows the object's type.
*
- *
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 the same subtype has multiple labels the first one is used when
- * encoding.
+ *
If an unknown subtype is encountered when decoding:
+ *
+ * if {@link #withDefaultValue(Object)} is used, then {@code defaultValue} will be returned
+ * if {@link #withFallbackJsonAdapter(JsonAdapter)} is used, then the
+ * {@code fallbackJsonAdapter.fromJson(reader)} result will be returned
+ * otherwise a {@link JsonDataException} will be thrown
+ *
*
- * 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.
+ *
If an unknown type is encountered when encoding:
+ *
+ * if {@link #withFallbackJsonAdapter(JsonAdapter)} is used, then the
+ * {@code fallbackJsonAdapter.toJson(writer, value)} result will be returned
+ * otherwise a {@link IllegalArgumentException} will be thrown
+ *
+ *
+ * If the same subtype has multiple labels the first one is used when encoding.
*/
public final class PolymorphicJsonAdapterFactory implements JsonAdapter.Factory {
+ /**
+ * Thin wrapper around {@link JsonAdapter} to allow {@link PolymorphicJsonAdapter} to
+ * distinguish between {@code JsonAdapter} added due to a {@code defaultValue} or added
+ * by users of {@link PolymorphicJsonAdapterFactory}.
+ */
+ private abstract static class DefaultJsonAdapter extends JsonAdapter {
+ }
+
final Class baseType;
final String labelKey;
final List labels;
final List subtypes;
- @Nullable final T defaultValue;
- final boolean defaultValueSet;
+ @Nullable final JsonAdapter fallbackJsonAdapter;
PolymorphicJsonAdapterFactory(
Class baseType,
String labelKey,
List labels,
List subtypes,
- @Nullable T defaultValue,
- boolean defaultValueSet) {
+ @Nullable JsonAdapter fallbackJsonAdapter) {
this.baseType = baseType;
this.labelKey = labelKey;
this.labels = labels;
this.subtypes = subtypes;
- this.defaultValue = defaultValue;
- this.defaultValueSet = defaultValueSet;
+ this.fallbackJsonAdapter = fallbackJsonAdapter;
}
/**
@@ -143,14 +157,11 @@ public final class PolymorphicJsonAdapterFactory implements JsonAdapter.Facto
labelKey,
Collections.emptyList(),
Collections.emptyList(),
- null,
- false);
+ null);
}
/**
- * Returns a new factory that decodes instances of {@code subtype}. When an unknown type is found
- * during encoding an {@linkplain IllegalArgumentException} will be thrown. When an unknown label
- * is found during decoding a {@linkplain JsonDataException} will be thrown.
+ * Returns a new factory that decodes instances of {@code subtype}.
*/
public PolymorphicJsonAdapterFactory withSubtype(Class extends T> subtype, String label) {
if (subtype == null) throw new NullPointerException("subtype == null");
@@ -166,21 +177,49 @@ public final class PolymorphicJsonAdapterFactory implements JsonAdapter.Facto
labelKey,
newLabels,
newSubtypes,
- defaultValue,
- defaultValueSet);
+ fallbackJsonAdapter);
}
/**
- * Returns a new factory that with default to {@code defaultValue} upon decoding of unrecognized
- * labels. The default value should be immutable.
+ * Returns a new factory that with default to {@code fallbackJsonAdapter.fromJson(reader)}
+ * upon decoding of unrecognized labels.
+ *
+ * The {@link JsonReader} instance will not be automatically consumed, so sure to consume it
+ * within your implementation of {@link JsonAdapter#fromJson(JsonReader)}
*/
- public PolymorphicJsonAdapterFactory withDefaultValue(@Nullable T defaultValue) {
+ public PolymorphicJsonAdapterFactory withFallbackJsonAdapter(
+ @Nullable JsonAdapter fallbackJsonAdapter) {
return new PolymorphicJsonAdapterFactory<>(baseType,
labelKey,
labels,
subtypes,
- defaultValue,
- true);
+ fallbackJsonAdapter);
+ }
+
+ /**
+ * Returns a new factory that will default to {@code defaultValue} upon decoding of unrecognized
+ * labels. The default value should be immutable.
+ */
+ public PolymorphicJsonAdapterFactory withDefaultValue(@Nullable T defaultValue) {
+ return withFallbackJsonAdapter(buildFallbackJsonAdapter(defaultValue));
+ }
+
+ private JsonAdapter buildFallbackJsonAdapter(final T defaultValue) {
+ return new DefaultJsonAdapter() {
+ @Nullable
+ @Override
+ public Object fromJson(JsonReader reader) throws IOException {
+ reader.skipValue();
+ return defaultValue;
+ }
+
+ @Override
+ public void toJson(JsonWriter writer, @Nullable Object value) throws IOException {
+ throw new IOException("This method should never be called. "
+ + "If you find this on your stacktraces please report it "
+ + "to the https://github.com/square/moshi project");
+ }
+ };
}
@Override
@@ -198,8 +237,7 @@ public final class PolymorphicJsonAdapterFactory implements JsonAdapter.Facto
labels,
subtypes,
jsonAdapters,
- defaultValue,
- defaultValueSet
+ fallbackJsonAdapter
).nullSafe();
}
@@ -208,8 +246,7 @@ public final class PolymorphicJsonAdapterFactory implements JsonAdapter.Facto
final List labels;
final List subtypes;
final List> jsonAdapters;
- @Nullable final Object defaultValue;
- final boolean defaultValueSet;
+ @Nullable final JsonAdapter fallbackJsonAdapter;
/** Single-element options containing the label's key only. */
final JsonReader.Options labelKeyOptions;
@@ -220,14 +257,12 @@ public final class PolymorphicJsonAdapterFactory implements JsonAdapter.Facto
List labels,
List subtypes,
List> jsonAdapters,
- @Nullable Object defaultValue,
- boolean defaultValueSet) {
+ @Nullable JsonAdapter fallbackJsonAdapter) {
this.labelKey = labelKey;
this.labels = labels;
this.subtypes = subtypes;
this.jsonAdapters = jsonAdapters;
- this.defaultValue = defaultValue;
- this.defaultValueSet = defaultValueSet;
+ this.fallbackJsonAdapter = fallbackJsonAdapter;
this.labelKeyOptions = JsonReader.Options.of(labelKey);
this.labelOptions = JsonReader.Options.of(labels.toArray(new String[0]));
@@ -243,10 +278,10 @@ public final class PolymorphicJsonAdapterFactory implements JsonAdapter.Facto
peeked.close();
}
if (labelIndex == -1) {
- reader.skipValue();
- return defaultValue;
+ return this.fallbackJsonAdapter.fromJson(reader);
+ } else {
+ return jsonAdapters.get(labelIndex).fromJson(reader);
}
- return jsonAdapters.get(labelIndex).fromJson(reader);
}
private int labelIndex(JsonReader reader) throws IOException {
@@ -259,7 +294,7 @@ public final class PolymorphicJsonAdapterFactory implements JsonAdapter.Facto
}
int labelIndex = reader.selectString(labelOptions);
- if (labelIndex == -1 && !defaultValueSet) {
+ if (labelIndex == -1 && this.fallbackJsonAdapter == null) {
throw new JsonDataException("Expected one of "
+ labels
+ " for key '"
@@ -277,18 +312,27 @@ public final class PolymorphicJsonAdapterFactory implements JsonAdapter.Facto
@Override public void toJson(JsonWriter writer, Object value) throws IOException {
Class> type = value.getClass();
int labelIndex = subtypes.indexOf(type);
+ final JsonAdapter adapter;
if (labelIndex == -1) {
- throw new IllegalArgumentException("Expected one of "
- + subtypes
- + " but found "
- + value
- + ", a "
- + value.getClass()
- + ". Register this subtype.");
+ if (fallbackJsonAdapter == null || fallbackJsonAdapter instanceof DefaultJsonAdapter) {
+ throw new IllegalArgumentException("Expected one of "
+ + subtypes
+ + " but found "
+ + value
+ + ", a "
+ + value.getClass()
+ + ". Register this subtype.");
+ } else {
+ adapter = fallbackJsonAdapter;
+ }
+ } else {
+ adapter = jsonAdapters.get(labelIndex);
}
- JsonAdapter adapter = jsonAdapters.get(labelIndex);
+
writer.beginObject();
- writer.name(labelKey).value(labels.get(labelIndex));
+ if (adapter != fallbackJsonAdapter) {
+ writer.name(labelKey).value(labels.get(labelIndex));
+ }
int flattenToken = writer.beginFlatten();
adapter.toJson(writer, value);
writer.endFlatten(flattenToken);
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 ffc479e..dd01ef1 100644
--- a/adapters/src/test/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactoryTest.java
+++ b/adapters/src/test/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactoryTest.java
@@ -18,6 +18,7 @@ package com.squareup.moshi.adapters;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonDataException;
import com.squareup.moshi.JsonReader;
+import com.squareup.moshi.JsonWriter;
import com.squareup.moshi.Moshi;
import java.io.IOException;
import java.util.Collections;
@@ -25,6 +26,8 @@ import java.util.Map;
import okio.Buffer;
import org.junit.Test;
+import javax.annotation.Nullable;
+
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
@@ -105,6 +108,35 @@ public final class PolymorphicJsonAdapterFactoryTest {
assertThat(message).isNull();
}
+ @Test public void specifiedFallbackJsonAdapter() throws IOException {
+ Moshi moshi = new Moshi.Builder()
+ .add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
+ .withSubtype(Success.class, "success")
+ .withSubtype(Error.class, "error")
+ .withFallbackJsonAdapter(new JsonAdapter() {
+ @Override
+ public Object fromJson(JsonReader reader) throws IOException {
+ reader.skipValue();
+ return new EmptyMessage();
+ }
+
+ @Override
+ public void toJson(JsonWriter writer, @Nullable Object value) {
+ throw new RuntimeException("Not implemented as not needed for the test");
+ }
+ })
+ )
+ .build();
+ JsonAdapter adapter = moshi.adapter(Message.class);
+
+ JsonReader reader =
+ JsonReader.of(new Buffer().writeUtf8("{\"type\":\"data\",\"value\":\"Okay!\"}"));
+
+ Message message = adapter.fromJson(reader);
+ assertThat(message).isInstanceOf(EmptyMessage.class);
+ assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
+ }
+
@Test public void unregisteredSubtype() {
Moshi moshi = new Moshi.Builder()
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
@@ -125,6 +157,52 @@ public final class PolymorphicJsonAdapterFactoryTest {
}
}
+ @Test public void unregisteredSubtypeWithDefaultValue() {
+ 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);
+
+ try {
+ adapter.toJson(new EmptyMessage());
+ } catch (IllegalArgumentException expected) {
+ assertThat(expected).hasMessage("Expected one of [class"
+ + " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$Success, class"
+ + " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$Error] but found"
+ + " EmptyMessage, a class"
+ + " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$EmptyMessage. Register"
+ + " this subtype.");
+ }
+ }
+
+ @Test public void unregisteredSubtypeWithFallbackJsonAdapter() {
+ Moshi moshi = new Moshi.Builder()
+ .add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
+ .withSubtype(Success.class, "success")
+ .withSubtype(Error.class, "error")
+ .withFallbackJsonAdapter(new JsonAdapter() {
+ @Nullable
+ @Override
+ public Object fromJson(JsonReader reader) {
+ throw new RuntimeException("Not implemented as not needed for the test");
+ }
+
+ @Override
+ public void toJson(JsonWriter writer, @Nullable Object value) throws IOException {
+ writer.name("type").value("injected by fallbackJsonAdapter");
+ }
+ }))
+ .build();
+ JsonAdapter adapter = moshi.adapter(Message.class);
+
+ String json = adapter.toJson(new EmptyMessage());
+ assertThat(json).isEqualTo("{\"type\":\"injected by fallbackJsonAdapter\"}");
+ }
+
@Test public void nonStringLabelValue() throws IOException {
Moshi moshi = new Moshi.Builder()
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")