mirror of
https://github.com/fankes/moshi.git
synced 2025-10-20 00:19:21 +08:00
Change RuntimeJsonAdapterFactory to peek for type names.
This is a bit awkward because JsonReader.Options doesn't tell you what its values are. Also awkward because we don't yet have an equivalent to stream the encode of the value.
This commit is contained in:
@@ -24,7 +24,10 @@ import com.squareup.moshi.Types;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import javax.annotation.CheckReturnValue;
|
import javax.annotation.CheckReturnValue;
|
||||||
@@ -39,8 +42,8 @@ import javax.annotation.CheckReturnValue;
|
|||||||
*
|
*
|
||||||
* Moshi moshi = new Moshi.Builder()
|
* Moshi moshi = new Moshi.Builder()
|
||||||
* .add(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
* .add(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||||
* .registerSubtype(Success.class, "success")
|
* .withSubtype(Success.class, "success")
|
||||||
* .registerSubtype(Error.class, "error"))
|
* .withSubtype(Error.class, "error"))
|
||||||
* .build();
|
* .build();
|
||||||
* }</pre>
|
* }</pre>
|
||||||
*/
|
*/
|
||||||
@@ -48,7 +51,16 @@ import javax.annotation.CheckReturnValue;
|
|||||||
final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
|
final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
|
||||||
final Class<T> baseType;
|
final Class<T> baseType;
|
||||||
final String labelKey;
|
final String labelKey;
|
||||||
final Map<String, Type> labelToType = new LinkedHashMap<>();
|
final List<String> labels;
|
||||||
|
final List<Type> subtypes;
|
||||||
|
|
||||||
|
RuntimeJsonAdapterFactory(
|
||||||
|
Class<T> baseType, String labelKey, List<String> labels, List<Type> subtypes) {
|
||||||
|
this.baseType = baseType;
|
||||||
|
this.labelKey = labelKey;
|
||||||
|
this.labels = labels;
|
||||||
|
this.subtypes = subtypes;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param baseType The base type for which this factory will create adapters. Cannot be Object.
|
* @param baseType The base type for which this factory will create adapters. Cannot be Object.
|
||||||
@@ -63,27 +75,26 @@ final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
|
|||||||
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<>(baseType, labelKey);
|
return new RuntimeJsonAdapterFactory<>(
|
||||||
}
|
baseType, labelKey, Collections.<String>emptyList(), Collections.<Type>emptyList());
|
||||||
|
|
||||||
RuntimeJsonAdapterFactory(Class<T> baseType, String labelKey) {
|
|
||||||
this.baseType = baseType;
|
|
||||||
this.labelKey = labelKey;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register the subtype that can be created based on the label. When an unknown type is found
|
* 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
|
* 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> registerSubtype(Class<? extends T> subtype, String label) {
|
public RuntimeJsonAdapterFactory<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 (labelToType.containsKey(label) || labelToType.containsValue(subtype)) {
|
if (labels.contains(label) || subtypes.contains(subtype)) {
|
||||||
throw new IllegalArgumentException("Subtypes and labels must be unique.");
|
throw new IllegalArgumentException("Subtypes and labels must be unique.");
|
||||||
}
|
}
|
||||||
labelToType.put(label, subtype);
|
List<String> newLabels = new ArrayList<>(labels);
|
||||||
return this;
|
newLabels.add(label);
|
||||||
|
List<Type> newSubtypes = new ArrayList<>(subtypes);
|
||||||
|
newSubtypes.add(subtype);
|
||||||
|
return new RuntimeJsonAdapterFactory<>(baseType, labelKey, newLabels, newSubtypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -91,84 +102,90 @@ final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
|
|||||||
if (Types.getRawType(type) != baseType || !annotations.isEmpty()) {
|
if (Types.getRawType(type) != baseType || !annotations.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
int size = labelToType.size();
|
|
||||||
Map<String, JsonAdapter<Object>> labelToAdapter = new LinkedHashMap<>(size);
|
List<JsonAdapter<Object>> jsonAdapters = new ArrayList<>();
|
||||||
Map<Type, String> typeToLabel = new LinkedHashMap<>(size);
|
for (int i = 0, size = subtypes.size(); i < size; i++) {
|
||||||
for (Map.Entry<String, Type> entry : labelToType.entrySet()) {
|
jsonAdapters.add(moshi.adapter(subtypes.get(i)));
|
||||||
String label = entry.getKey();
|
|
||||||
Type typeValue = entry.getValue();
|
|
||||||
typeToLabel.put(typeValue, label);
|
|
||||||
labelToAdapter.put(label, moshi.adapter(typeValue));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonAdapter<Object> objectJsonAdapter = moshi.adapter(Object.class);
|
JsonAdapter<Object> objectJsonAdapter = moshi.adapter(Object.class);
|
||||||
return new RuntimeJsonAdapter(labelKey, labelToAdapter, typeToLabel,
|
return new RuntimeJsonAdapter(
|
||||||
objectJsonAdapter).nullSafe();
|
labelKey, labels, subtypes, jsonAdapters, objectJsonAdapter).nullSafe();
|
||||||
}
|
}
|
||||||
|
|
||||||
static final class RuntimeJsonAdapter extends JsonAdapter<Object> {
|
static final class RuntimeJsonAdapter extends JsonAdapter<Object> {
|
||||||
final String labelKey;
|
final String labelKey;
|
||||||
final Map<String, JsonAdapter<Object>> labelToAdapter;
|
final List<String> labels;
|
||||||
final Map<Type, String> typeToLabel;
|
final List<Type> subtypes;
|
||||||
|
final List<JsonAdapter<Object>> jsonAdapters;
|
||||||
final JsonAdapter<Object> objectJsonAdapter;
|
final JsonAdapter<Object> objectJsonAdapter;
|
||||||
|
|
||||||
RuntimeJsonAdapter(String labelKey, Map<String, JsonAdapter<Object>> labelToAdapter,
|
/** Single-element options containing the label's key only. */
|
||||||
Map<Type, String> typeToLabel, JsonAdapter<Object> objectJsonAdapter) {
|
final JsonReader.Options labelKeyOptions;
|
||||||
|
/** Corresponds to subtypes. */
|
||||||
|
final JsonReader.Options labelOptions;
|
||||||
|
|
||||||
|
RuntimeJsonAdapter(String labelKey, List<String> labels,
|
||||||
|
List<Type> subtypes, List<JsonAdapter<Object>> jsonAdapters,
|
||||||
|
JsonAdapter<Object> objectJsonAdapter) {
|
||||||
this.labelKey = labelKey;
|
this.labelKey = labelKey;
|
||||||
this.labelToAdapter = labelToAdapter;
|
this.labels = labels;
|
||||||
this.typeToLabel = typeToLabel;
|
this.subtypes = subtypes;
|
||||||
|
this.jsonAdapters = jsonAdapters;
|
||||||
this.objectJsonAdapter = objectJsonAdapter;
|
this.objectJsonAdapter = objectJsonAdapter;
|
||||||
|
|
||||||
|
this.labelKeyOptions = JsonReader.Options.of(labelKey);
|
||||||
|
this.labelOptions = JsonReader.Options.of(labels.toArray(new String[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public Object fromJson(JsonReader reader) throws IOException {
|
@Override public Object fromJson(JsonReader reader) throws IOException {
|
||||||
JsonReader.Token peekedToken = reader.peek();
|
int labelIndex = labelIndex(reader.peekJson());
|
||||||
if (peekedToken != JsonReader.Token.BEGIN_OBJECT) {
|
return jsonAdapters.get(labelIndex).fromJson(reader);
|
||||||
throw new JsonDataException("Expected BEGIN_OBJECT but was " + peekedToken
|
}
|
||||||
+ " at path " + reader.getPath());
|
|
||||||
|
private int labelIndex(JsonReader reader) throws IOException {
|
||||||
|
reader.beginObject();
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
if (reader.selectName(labelKeyOptions) == -1) {
|
||||||
|
reader.skipName();
|
||||||
|
reader.skipValue();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int labelIndex = reader.selectString(labelOptions);
|
||||||
|
if (labelIndex == -1) {
|
||||||
|
throw new JsonDataException("Expected one of "
|
||||||
|
+ labels
|
||||||
|
+ " for key '"
|
||||||
|
+ labelKey
|
||||||
|
+ "' but found '"
|
||||||
|
+ reader.nextString()
|
||||||
|
+ "'. Register a subtype for this label.");
|
||||||
|
}
|
||||||
|
reader.close();
|
||||||
|
return labelIndex;
|
||||||
}
|
}
|
||||||
Object jsonValue = reader.readJsonValue();
|
|
||||||
Map<String, Object> jsonObject = (Map<String, Object>) jsonValue;
|
throw new JsonDataException("Missing label for " + labelKey);
|
||||||
Object label = jsonObject.get(labelKey);
|
|
||||||
if (label == null) {
|
|
||||||
throw new JsonDataException("Missing label for " + labelKey);
|
|
||||||
}
|
|
||||||
if (!(label instanceof String)) {
|
|
||||||
throw new JsonDataException("Label for '"
|
|
||||||
+ labelKey
|
|
||||||
+ "' must be a string but was "
|
|
||||||
+ label
|
|
||||||
+ ", a "
|
|
||||||
+ label.getClass());
|
|
||||||
}
|
|
||||||
JsonAdapter<Object> adapter = labelToAdapter.get(label);
|
|
||||||
if (adapter == null) {
|
|
||||||
throw new JsonDataException("Expected one of "
|
|
||||||
+ labelToAdapter.keySet()
|
|
||||||
+ " for key '"
|
|
||||||
+ labelKey
|
|
||||||
+ "' but found '"
|
|
||||||
+ label
|
|
||||||
+ "'. Register a subtype for this label.");
|
|
||||||
}
|
|
||||||
return adapter.fromJsonValue(jsonValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public void toJson(JsonWriter writer, Object value) throws IOException {
|
@Override public void toJson(JsonWriter writer, Object value) throws IOException {
|
||||||
Class<?> type = value.getClass();
|
Class<?> type = value.getClass();
|
||||||
String label = typeToLabel.get(type);
|
int labelIndex = subtypes.indexOf(type);
|
||||||
if (label == null) {
|
if (labelIndex == -1) {
|
||||||
throw new IllegalArgumentException("Expected one of "
|
throw new IllegalArgumentException("Expected one of "
|
||||||
+ typeToLabel.keySet()
|
+ subtypes
|
||||||
+ " but found "
|
+ " but found "
|
||||||
+ value
|
+ value
|
||||||
+ ", a "
|
+ ", a "
|
||||||
+ value.getClass()
|
+ value.getClass()
|
||||||
+ ". Register this subtype.");
|
+ ". Register this subtype.");
|
||||||
}
|
}
|
||||||
JsonAdapter<Object> adapter = labelToAdapter.get(label);
|
JsonAdapter<Object> adapter = jsonAdapters.get(labelIndex);
|
||||||
Map<String, Object> jsonValue = (Map<String, Object>) adapter.toJsonValue(value);
|
Map<String, Object> jsonValue = (Map<String, Object>) adapter.toJsonValue(value);
|
||||||
|
|
||||||
Map<String, Object> valueWithLabel = new LinkedHashMap<>(1 + jsonValue.size());
|
Map<String, Object> valueWithLabel = new LinkedHashMap<>(1 + jsonValue.size());
|
||||||
valueWithLabel.put(labelKey, label);
|
valueWithLabel.put(labelKey, labels.get(labelIndex));
|
||||||
valueWithLabel.putAll(jsonValue);
|
valueWithLabel.putAll(jsonValue);
|
||||||
objectJsonAdapter.toJson(writer, valueWithLabel);
|
objectJsonAdapter.toJson(writer, valueWithLabel);
|
||||||
}
|
}
|
||||||
|
@@ -33,8 +33,8 @@ public final class RuntimeJsonAdapterFactoryTest {
|
|||||||
@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(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||||
.registerSubtype(Success.class, "success")
|
.withSubtype(Success.class, "success")
|
||||||
.registerSubtype(Error.class, "error"))
|
.withSubtype(Error.class, "error"))
|
||||||
.build();
|
.build();
|
||||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||||
|
|
||||||
@@ -47,8 +47,8 @@ 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(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||||
.registerSubtype(Success.class, "success")
|
.withSubtype(Success.class, "success")
|
||||||
.registerSubtype(Error.class, "error"))
|
.withSubtype(Error.class, "error"))
|
||||||
.build();
|
.build();
|
||||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||||
|
|
||||||
@@ -61,8 +61,8 @@ 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(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||||
.registerSubtype(Success.class, "success")
|
.withSubtype(Success.class, "success")
|
||||||
.registerSubtype(Error.class, "error"))
|
.withSubtype(Error.class, "error"))
|
||||||
.build();
|
.build();
|
||||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||||
|
|
||||||
@@ -75,14 +75,14 @@ public final class RuntimeJsonAdapterFactoryTest {
|
|||||||
assertThat(expected).hasMessage("Expected one of [success, error] for key 'type' but found"
|
assertThat(expected).hasMessage("Expected one of [success, error] for key 'type' but found"
|
||||||
+ " 'data'. Register a subtype for this label.");
|
+ " 'data'. Register a subtype for this label.");
|
||||||
}
|
}
|
||||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_OBJECT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@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(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||||
.registerSubtype(Success.class, "success")
|
.withSubtype(Success.class, "success")
|
||||||
.registerSubtype(Error.class, "error"))
|
.withSubtype(Error.class, "error"))
|
||||||
.build();
|
.build();
|
||||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||||
|
|
||||||
@@ -101,8 +101,8 @@ public final class RuntimeJsonAdapterFactoryTest {
|
|||||||
@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(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||||
.registerSubtype(Success.class, "success")
|
.withSubtype(Success.class, "success")
|
||||||
.registerSubtype(Error.class, "error"))
|
.withSubtype(Error.class, "error"))
|
||||||
.build();
|
.build();
|
||||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||||
|
|
||||||
@@ -110,16 +110,15 @@ public final class RuntimeJsonAdapterFactoryTest {
|
|||||||
adapter.fromJson("{\"type\":{},\"value\":\"Okay!\"}");
|
adapter.fromJson("{\"type\":{},\"value\":\"Okay!\"}");
|
||||||
fail();
|
fail();
|
||||||
} catch (JsonDataException expected) {
|
} catch (JsonDataException expected) {
|
||||||
assertThat(expected).hasMessage("Label for 'type' must be a string but was {},"
|
assertThat(expected).hasMessage("Expected a string but was BEGIN_OBJECT at path $.type");
|
||||||
+ " a class com.squareup.moshi.LinkedHashTreeMap");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||||
.registerSubtype(Success.class, "success")
|
.withSubtype(Success.class, "success")
|
||||||
.registerSubtype(Error.class, "error"))
|
.withSubtype(Error.class, "error"))
|
||||||
.build();
|
.build();
|
||||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||||
|
|
||||||
@@ -137,9 +136,9 @@ public final class RuntimeJsonAdapterFactoryTest {
|
|||||||
@Test public void uniqueSubtypes() {
|
@Test public void uniqueSubtypes() {
|
||||||
RuntimeJsonAdapterFactory<Message> factory =
|
RuntimeJsonAdapterFactory<Message> factory =
|
||||||
RuntimeJsonAdapterFactory.of(Message.class, "type")
|
RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||||
.registerSubtype(Success.class, "success");
|
.withSubtype(Success.class, "success");
|
||||||
try {
|
try {
|
||||||
factory.registerSubtype(Success.class, "data");
|
factory.withSubtype(Success.class, "data");
|
||||||
fail();
|
fail();
|
||||||
} catch (IllegalArgumentException expected) {
|
} catch (IllegalArgumentException expected) {
|
||||||
assertThat(expected).hasMessage("Subtypes and labels must be unique.");
|
assertThat(expected).hasMessage("Subtypes and labels must be unique.");
|
||||||
@@ -149,9 +148,9 @@ public final class RuntimeJsonAdapterFactoryTest {
|
|||||||
@Test public void uniqueLabels() {
|
@Test public void uniqueLabels() {
|
||||||
RuntimeJsonAdapterFactory<Message> factory =
|
RuntimeJsonAdapterFactory<Message> factory =
|
||||||
RuntimeJsonAdapterFactory.of(Message.class, "type")
|
RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||||
.registerSubtype(Success.class, "data");
|
.withSubtype(Success.class, "data");
|
||||||
try {
|
try {
|
||||||
factory.registerSubtype(Error.class, "data");
|
factory.withSubtype(Error.class, "data");
|
||||||
fail();
|
fail();
|
||||||
} catch (IllegalArgumentException expected) {
|
} catch (IllegalArgumentException expected) {
|
||||||
assertThat(expected).hasMessage("Subtypes and labels must be unique.");
|
assertThat(expected).hasMessage("Subtypes and labels must be unique.");
|
||||||
@@ -161,8 +160,8 @@ 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(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||||
.registerSubtype(Success.class, "success")
|
.withSubtype(Success.class, "success")
|
||||||
.registerSubtype(Error.class, "error"))
|
.withSubtype(Error.class, "error"))
|
||||||
.build();
|
.build();
|
||||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user