Merge pull request #713 from square/jwilson.1019.runtime_polymorphic

Rename RuntimeJsonAdapterFactory to PolymorphicJsonAdapterFactory
This commit is contained in:
Jesse Wilson
2018-10-20 00:54:14 -04:00
committed by GitHub
2 changed files with 91 additions and 35 deletions

View File

@@ -31,28 +31,84 @@ import java.util.Set;
import javax.annotation.CheckReturnValue; import javax.annotation.CheckReturnValue;
/** /**
* A JsonAdapter factory for polymorphic types. This is useful when the type is not known before * A JsonAdapter factory for objects that include type information in the JSON. When decoding JSON
* decoding the JSON. This factory's adapters expect JSON in the format of a JSON object with a * Moshi uses this type information to determine which class to decode to. When encoding Moshi uses
* key whose value is a label that determines the type to which to map the JSON object. To use, add * the objects class to determine what type information to include.
* this factory to your {@link Moshi.Builder}: *
* <p>Suppose we have an interface, its implementations, and a class that uses them:
*
* <pre> {@code
*
* interface HandOfCards {
* }
*
* class BlackjackHand extends HandOfCards {
* Card hidden_card;
* List<Card> visible_cards;
* }
*
* class HoldemHand extends HandOfCards {
* Set<Card> hidden_cards;
* }
*
* class Player {
* String name;
* HandOfCards hand;
* }
* }</pre>
*
* <p>We want to decode the following JSON into the player model above:
*
* <pre> {@code
*
* {
* "name": "Jesse",
* "hand": {
* "hand_type": "blackjack",
* "hidden_card": "9D",
* "visible_cards": ["8H", "4C"]
* }
* }
* }</pre>
*
* <p>Left unconfigured, Moshi would incorrectly attempt to decode the hand object to the abstract
* {@code HandOfCards} interface. We configure it to use the appropriate subtype instead:
* *
* <pre> {@code * <pre> {@code
* *
* Moshi moshi = new Moshi.Builder() * Moshi moshi = new Moshi.Builder()
* .add(RuntimeJsonAdapterFactory.of(Message.class, "type") * .add(PolymorphicJsonAdapterFactory.of(HandOfCards.class, "hand_type")
* .withSubtype(Success.class, "success") * .withSubtype(BlackjackHand.class, "blackjack")
* .withSubtype(Error.class, "error")) * .withSubtype(HoldemHand.class, "holdem"))
* .build(); * .build();
* }</pre> * }</pre>
*
* <p>This class imposes strict requirements on its use:
*
* <ul>
* <li>Base types may be classes or interfaces. You may not use {@code Object.class} as a base
* type.
* <li>Subtypes must encode as JSON objects.
* <li>Type information must be in the encoded object. Each message must have a type label like
* {@code hand_type} whose value is a string like {@code blackjack} that identifies which type
* to use.
* <li>Each type identifier must be unique.
* </ul>
*
* <p>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.
*
* <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
* IllegalArgumentException}.
*/ */
public final class PolymorphicJsonAdapterFactory<T> implements JsonAdapter.Factory {
public final class RuntimeJsonAdapterFactory<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;
RuntimeJsonAdapterFactory( PolymorphicJsonAdapterFactory(
Class<T> baseType, String labelKey, List<String> labels, List<Type> subtypes) { Class<T> baseType, String labelKey, List<String> labels, List<Type> subtypes) {
this.baseType = baseType; this.baseType = baseType;
this.labelKey = labelKey; this.labelKey = labelKey;
@@ -66,14 +122,14 @@ public final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
* JSON object. * JSON object.
*/ */
@CheckReturnValue @CheckReturnValue
public static <T> RuntimeJsonAdapterFactory<T> of(Class<T> baseType, String labelKey) { public static <T> PolymorphicJsonAdapterFactory<T> of(Class<T> baseType, String labelKey) {
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");
if (baseType == Object.class) { if (baseType == Object.class) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"The base type must not be Object. Consider using a marker interface."); "The base type must not be Object. Consider using a marker interface.");
} }
return new RuntimeJsonAdapterFactory<>( return new PolymorphicJsonAdapterFactory<>(
baseType, labelKey, Collections.<String>emptyList(), Collections.<Type>emptyList()); baseType, labelKey, Collections.<String>emptyList(), Collections.<Type>emptyList());
} }
@@ -82,7 +138,7 @@ public final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
* during encoding an {@linkplain IllegalArgumentException} will be thrown. When an unknown label * during encoding an {@linkplain IllegalArgumentException} will be thrown. When an unknown label
* is found during decoding a {@linkplain JsonDataException} will be thrown. * is found during decoding a {@linkplain JsonDataException} will be thrown.
*/ */
public RuntimeJsonAdapterFactory<T> withSubtype(Class<? extends T> subtype, String label) { public PolymorphicJsonAdapterFactory<T> withSubtype(Class<? extends T> subtype, String label) {
if (subtype == null) throw new NullPointerException("subtype == null"); if (subtype == null) throw new NullPointerException("subtype == null");
if (label == null) throw new NullPointerException("label == null"); if (label == null) throw new NullPointerException("label == null");
if (labels.contains(label) || subtypes.contains(subtype)) { if (labels.contains(label) || subtypes.contains(subtype)) {
@@ -92,7 +148,7 @@ public final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
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 RuntimeJsonAdapterFactory<>(baseType, labelKey, newLabels, newSubtypes); return new PolymorphicJsonAdapterFactory<>(baseType, labelKey, newLabels, newSubtypes);
} }
@Override @Override
@@ -107,11 +163,11 @@ public final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
} }
JsonAdapter<Object> objectJsonAdapter = moshi.adapter(Object.class); JsonAdapter<Object> objectJsonAdapter = moshi.adapter(Object.class);
return new RuntimeJsonAdapter( return new PolymorphicJsonAdapter(
labelKey, labels, subtypes, jsonAdapters, objectJsonAdapter).nullSafe(); labelKey, labels, subtypes, jsonAdapters, objectJsonAdapter).nullSafe();
} }
static final class RuntimeJsonAdapter extends JsonAdapter<Object> { static final class PolymorphicJsonAdapter extends JsonAdapter<Object> {
final String labelKey; final String labelKey;
final List<String> labels; final List<String> labels;
final List<Type> subtypes; final List<Type> subtypes;
@@ -123,7 +179,7 @@ public final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
/** Corresponds to subtypes. */ /** Corresponds to subtypes. */
final JsonReader.Options labelOptions; final JsonReader.Options labelOptions;
RuntimeJsonAdapter(String labelKey, List<String> labels, PolymorphicJsonAdapter(String labelKey, List<String> labels,
List<Type> subtypes, List<JsonAdapter<Object>> jsonAdapters, List<Type> subtypes, List<JsonAdapter<Object>> jsonAdapters,
JsonAdapter<Object> objectJsonAdapter) { JsonAdapter<Object> objectJsonAdapter) {
this.labelKey = labelKey; this.labelKey = labelKey;
@@ -189,7 +245,7 @@ public final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
} }
@Override public String toString() { @Override public String toString() {
return "RuntimeJsonAdapter(" + labelKey + ")"; return "PolymorphicJsonAdapter(" + labelKey + ")";
} }
} }
} }

View File

@@ -29,10 +29,10 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
@SuppressWarnings("CheckReturnValue") @SuppressWarnings("CheckReturnValue")
public final class RuntimeJsonAdapterFactoryTest { public final class PolymorphicJsonAdapterFactoryTest {
@Test public void fromJson() throws IOException { @Test public void fromJson() throws IOException {
Moshi moshi = new Moshi.Builder() Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type") .add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success") .withSubtype(Success.class, "success")
.withSubtype(Error.class, "error")) .withSubtype(Error.class, "error"))
.build(); .build();
@@ -46,7 +46,7 @@ public final class RuntimeJsonAdapterFactoryTest {
@Test public void toJson() { @Test public void toJson() {
Moshi moshi = new Moshi.Builder() Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type") .add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success") .withSubtype(Success.class, "success")
.withSubtype(Error.class, "error")) .withSubtype(Error.class, "error"))
.build(); .build();
@@ -60,7 +60,7 @@ public final class RuntimeJsonAdapterFactoryTest {
@Test public void unregisteredLabelValue() throws IOException { @Test public void unregisteredLabelValue() throws IOException {
Moshi moshi = new Moshi.Builder() Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type") .add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success") .withSubtype(Success.class, "success")
.withSubtype(Error.class, "error")) .withSubtype(Error.class, "error"))
.build(); .build();
@@ -80,7 +80,7 @@ public final class RuntimeJsonAdapterFactoryTest {
@Test public void unregisteredSubtype() { @Test public void unregisteredSubtype() {
Moshi moshi = new Moshi.Builder() Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type") .add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success") .withSubtype(Success.class, "success")
.withSubtype(Error.class, "error")) .withSubtype(Error.class, "error"))
.build(); .build();
@@ -90,17 +90,17 @@ public final class RuntimeJsonAdapterFactoryTest {
adapter.toJson(new EmptyMessage()); adapter.toJson(new EmptyMessage());
} catch (IllegalArgumentException expected) { } catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("Expected one of [class" assertThat(expected).hasMessage("Expected one of [class"
+ " com.squareup.moshi.adapters.RuntimeJsonAdapterFactoryTest$Success, class" + " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$Success, class"
+ " com.squareup.moshi.adapters.RuntimeJsonAdapterFactoryTest$Error] but found" + " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$Error] but found"
+ " EmptyMessage, a class" + " EmptyMessage, a class"
+ " com.squareup.moshi.adapters.RuntimeJsonAdapterFactoryTest$EmptyMessage. Register" + " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$EmptyMessage. Register"
+ " this subtype."); + " this subtype.");
} }
} }
@Test public void nonStringLabelValue() throws IOException { @Test public void nonStringLabelValue() throws IOException {
Moshi moshi = new Moshi.Builder() Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type") .add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success") .withSubtype(Success.class, "success")
.withSubtype(Error.class, "error")) .withSubtype(Error.class, "error"))
.build(); .build();
@@ -116,7 +116,7 @@ public final class RuntimeJsonAdapterFactoryTest {
@Test public void nonObjectDoesNotConsume() throws IOException { @Test public void nonObjectDoesNotConsume() throws IOException {
Moshi moshi = new Moshi.Builder() Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type") .add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success") .withSubtype(Success.class, "success")
.withSubtype(Error.class, "error")) .withSubtype(Error.class, "error"))
.build(); .build();
@@ -134,8 +134,8 @@ public final class RuntimeJsonAdapterFactoryTest {
} }
@Test public void uniqueSubtypes() { @Test public void uniqueSubtypes() {
RuntimeJsonAdapterFactory<Message> factory = PolymorphicJsonAdapterFactory<Message> factory =
RuntimeJsonAdapterFactory.of(Message.class, "type") PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success"); .withSubtype(Success.class, "success");
try { try {
factory.withSubtype(Success.class, "data"); factory.withSubtype(Success.class, "data");
@@ -146,8 +146,8 @@ public final class RuntimeJsonAdapterFactoryTest {
} }
@Test public void uniqueLabels() { @Test public void uniqueLabels() {
RuntimeJsonAdapterFactory<Message> factory = PolymorphicJsonAdapterFactory<Message> factory =
RuntimeJsonAdapterFactory.of(Message.class, "type") PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "data"); .withSubtype(Success.class, "data");
try { try {
factory.withSubtype(Error.class, "data"); factory.withSubtype(Error.class, "data");
@@ -159,7 +159,7 @@ public final class RuntimeJsonAdapterFactoryTest {
@Test public void nullSafe() throws IOException { @Test public void nullSafe() throws IOException {
Moshi moshi = new Moshi.Builder() Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type") .add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success") .withSubtype(Success.class, "success")
.withSubtype(Error.class, "error")) .withSubtype(Error.class, "error"))
.build(); .build();
@@ -172,7 +172,7 @@ public final class RuntimeJsonAdapterFactoryTest {
@Test public void disallowObjectBaseType() { @Test public void disallowObjectBaseType() {
try { try {
RuntimeJsonAdapterFactory.of(Object.class, "type"); PolymorphicJsonAdapterFactory.of(Object.class, "type");
fail(); fail();
} catch (IllegalArgumentException expected) { } catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage( assertThat(expected).hasMessage(
@@ -186,7 +186,7 @@ public final class RuntimeJsonAdapterFactoryTest {
*/ */
@Test public void unportableTypes() throws IOException { @Test public void unportableTypes() throws IOException {
Moshi moshi = new Moshi.Builder() Moshi moshi = new Moshi.Builder()
.add(RuntimeJsonAdapterFactory.of(Message.class, "type") .add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(MessageWithUnportableTypes.class, "unportable")) .withSubtype(MessageWithUnportableTypes.class, "unportable"))
.build(); .build();
JsonAdapter<Message> adapter = moshi.adapter(Message.class); JsonAdapter<Message> adapter = moshi.adapter(Message.class);