mirror of
https://github.com/fankes/moshi.git
synced 2025-10-20 00:19:21 +08:00
Merge pull request #52 from square/jwilson_0613_object_adapter
Runtime type adapter.
This commit is contained in:
@@ -24,9 +24,6 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
// TODO: support @Nullable
|
|
||||||
// TODO: path in JsonWriter.
|
|
||||||
|
|
||||||
final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
||||||
private final List<AdapterMethod> toAdapters;
|
private final List<AdapterMethod> toAdapters;
|
||||||
private final List<AdapterMethod> fromAdapters;
|
private final List<AdapterMethod> fromAdapters;
|
||||||
|
@@ -1198,11 +1198,8 @@ public final class JsonReader implements Closeable {
|
|||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
} else if (c == '#') {
|
} else if (c == '#') {
|
||||||
/*
|
// Skip a # hash end-of-line comment. The JSON RFC doesn't specify this behaviour, but it's
|
||||||
* Skip a # hash end-of-line comment. The JSON RFC doesn't
|
// required to parse existing documents. See http://b/2571423.
|
||||||
* specify this behaviour, but it's required to parse
|
|
||||||
* existing documents. See http://b/2571423.
|
|
||||||
*/
|
|
||||||
checkLenient();
|
checkLenient();
|
||||||
skipToEndOfLine();
|
skipToEndOfLine();
|
||||||
p = 0;
|
p = 0;
|
||||||
|
@@ -41,11 +41,11 @@ public final class Moshi {
|
|||||||
this.factories = Collections.unmodifiableList(factories);
|
this.factories = Collections.unmodifiableList(factories);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns a JSON adapter for {@code type}, creating it if necessary. */
|
||||||
public <T> JsonAdapter<T> adapter(Type type) {
|
public <T> JsonAdapter<T> adapter(Type type) {
|
||||||
return adapter(type, Util.NO_ANNOTATIONS);
|
return adapter(type, Util.NO_ANNOTATIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns a JSON adapter for {@code type}, creating it if necessary. */
|
|
||||||
public <T> JsonAdapter<T> adapter(Class<T> type) {
|
public <T> JsonAdapter<T> adapter(Class<T> type) {
|
||||||
// TODO: cache created JSON adapters.
|
// TODO: cache created JSON adapters.
|
||||||
return adapter(type, Util.NO_ANNOTATIONS);
|
return adapter(type, Util.NO_ANNOTATIONS);
|
||||||
@@ -60,10 +60,6 @@ public final class Moshi {
|
|||||||
return createAdapter(factories.indexOf(skipPast) + 1, type, annotations);
|
return createAdapter(factories.indexOf(skipPast) + 1, type, annotations);
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T> JsonAdapter<T> nextAdapter(JsonAdapter.Factory skipPast, Type type) {
|
|
||||||
return nextAdapter(skipPast, type, Util.NO_ANNOTATIONS);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters.
|
@SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters.
|
||||||
private <T> JsonAdapter<T> createAdapter(
|
private <T> JsonAdapter<T> createAdapter(
|
||||||
int firstIndex, Type type, Set<? extends Annotation> annotations) {
|
int firstIndex, Type type, Set<? extends Annotation> annotations) {
|
||||||
@@ -125,7 +121,10 @@ public final class Moshi {
|
|||||||
@Override public JsonAdapter<?> create(
|
@Override public JsonAdapter<?> create(
|
||||||
Type targetType, Set<? extends Annotation> annotations, Moshi moshi) {
|
Type targetType, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||||
if (!Util.typesMatch(type, targetType)) return null;
|
if (!Util.typesMatch(type, targetType)) return null;
|
||||||
|
|
||||||
|
// TODO: check for an annotations exact match.
|
||||||
if (!Util.isAnnotationPresent(annotations, annotation)) return null;
|
if (!Util.isAnnotationPresent(annotations, annotation)) return null;
|
||||||
|
|
||||||
return jsonAdapter;
|
return jsonAdapter;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -18,7 +18,11 @@ package com.squareup.moshi;
|
|||||||
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.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
final class StandardJsonAdapters {
|
final class StandardJsonAdapters {
|
||||||
@@ -43,6 +47,7 @@ final class StandardJsonAdapters {
|
|||||||
if (type == Long.class) return LONG_JSON_ADAPTER.nullSafe();
|
if (type == Long.class) return LONG_JSON_ADAPTER.nullSafe();
|
||||||
if (type == Short.class) return SHORT_JSON_ADAPTER.nullSafe();
|
if (type == Short.class) return SHORT_JSON_ADAPTER.nullSafe();
|
||||||
if (type == String.class) return STRING_JSON_ADAPTER.nullSafe();
|
if (type == String.class) return STRING_JSON_ADAPTER.nullSafe();
|
||||||
|
if (type == Object.class) return new ObjectJsonAdapter(moshi).nullSafe();
|
||||||
|
|
||||||
Class<?> rawType = Types.getRawType(type);
|
Class<?> rawType = Types.getRawType(type);
|
||||||
if (rawType.isEnum()) {
|
if (rawType.isEnum()) {
|
||||||
@@ -189,4 +194,79 @@ final class StandardJsonAdapters {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This adapter is used when the declared type is {@code java.lang.Object}. Typically the runtime
|
||||||
|
* type is something else, and when encoding JSON this delegates to the runtime type's adapter.
|
||||||
|
* For decoding (where there is no runtime type to inspect), this uses maps and lists.
|
||||||
|
*
|
||||||
|
* <p>This adapter needs a Moshi instance to look up the appropriate adapter for runtime types as
|
||||||
|
* they are encountered.
|
||||||
|
*/
|
||||||
|
static final class ObjectJsonAdapter extends JsonAdapter<Object> {
|
||||||
|
private final Moshi moshi;
|
||||||
|
|
||||||
|
public ObjectJsonAdapter(Moshi moshi) {
|
||||||
|
this.moshi = moshi;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public Object fromJson(JsonReader reader) throws IOException {
|
||||||
|
switch (reader.peek()) {
|
||||||
|
case BEGIN_ARRAY:
|
||||||
|
List<Object> list = new ArrayList<>();
|
||||||
|
reader.beginArray();
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
list.add(fromJson(reader));
|
||||||
|
}
|
||||||
|
reader.endArray();
|
||||||
|
return list;
|
||||||
|
|
||||||
|
case BEGIN_OBJECT:
|
||||||
|
Map<String, Object> map = new LinkedHashTreeMap<>();
|
||||||
|
reader.beginObject();
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
map.put(reader.nextName(), fromJson(reader));
|
||||||
|
}
|
||||||
|
reader.endObject();
|
||||||
|
return map;
|
||||||
|
|
||||||
|
case STRING:
|
||||||
|
return reader.nextString();
|
||||||
|
|
||||||
|
case NUMBER:
|
||||||
|
return reader.nextDouble();
|
||||||
|
|
||||||
|
case BOOLEAN:
|
||||||
|
return reader.nextBoolean();
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException("Expected a value but was " + reader.peek()
|
||||||
|
+ " at path " + reader.getPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void toJson(JsonWriter writer, Object value) throws IOException {
|
||||||
|
Class<?> valueClass = value.getClass();
|
||||||
|
if (valueClass == Object.class) {
|
||||||
|
// Don't recurse infinitely when the runtime type is also Object.class.
|
||||||
|
writer.beginObject();
|
||||||
|
writer.endObject();
|
||||||
|
} else {
|
||||||
|
moshi.adapter(toJsonType(valueClass), Util.NO_ANNOTATIONS).toJson(writer, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the type to look up a type adapter for when writing {@code value} to JSON. Without
|
||||||
|
* this, attempts to emit standard types like `LinkedHashMap` would fail because Moshi doesn't
|
||||||
|
* provide built-in adapters for implementation types. It knows how to <strong>write</strong>
|
||||||
|
* those types, but lacks a mechanism to read them because it doesn't know how to find the
|
||||||
|
* appropriate constructor.
|
||||||
|
*/
|
||||||
|
private Class<?> toJsonType(Class<?> valueClass) {
|
||||||
|
if (Map.class.isAssignableFrom(valueClass)) return Map.class;
|
||||||
|
if (Collection.class.isAssignableFrom(valueClass)) return Collection.class;
|
||||||
|
return valueClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -48,18 +48,6 @@ final class Types {
|
|||||||
return new ParameterizedTypeImpl(null, rawType, typeArguments);
|
return new ParameterizedTypeImpl(null, rawType, typeArguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a new parameterized type, applying {@code typeArguments} to {@code rawType} and
|
|
||||||
* enclosed by {@code ownerType}.
|
|
||||||
*/
|
|
||||||
public static ParameterizedType newParameterizedTypeWithOwner(
|
|
||||||
Type ownerType, Type rawType, Type... typeArguments) {
|
|
||||||
if (ownerType == null) {
|
|
||||||
throw new NullPointerException("ownerType");
|
|
||||||
}
|
|
||||||
return new ParameterizedTypeImpl(ownerType, rawType, typeArguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns an array type whose elements are all instances of {@code componentType}. */
|
/** Returns an array type whose elements are all instances of {@code componentType}. */
|
||||||
public static GenericArrayType arrayOf(Type componentType) {
|
public static GenericArrayType arrayOf(Type componentType) {
|
||||||
return new GenericArrayTypeImpl(componentType);
|
return new GenericArrayTypeImpl(componentType);
|
||||||
|
@@ -113,7 +113,7 @@ public final class CircularAdaptersTest {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final JsonAdapter<Node> delegate = moshi.nextAdapter(this, Node.class);
|
final JsonAdapter<Node> delegate = moshi.nextAdapter(this, Node.class, Util.NO_ANNOTATIONS);
|
||||||
|
|
||||||
return new JsonAdapter<Node>() {
|
return new JsonAdapter<Node>() {
|
||||||
@Override public void toJson(JsonWriter writer, Node value) throws IOException {
|
@Override public void toJson(JsonWriter writer, Node value) throws IOException {
|
||||||
|
@@ -35,7 +35,6 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
public final class MoshiTest {
|
public final class MoshiTest {
|
||||||
|
|
||||||
@Test public void booleanAdapter() throws Exception {
|
@Test public void booleanAdapter() throws Exception {
|
||||||
Moshi moshi = new Moshi.Builder().build();
|
Moshi moshi = new Moshi.Builder().build();
|
||||||
JsonAdapter<Boolean> adapter = moshi.adapter(boolean.class).lenient();
|
JsonAdapter<Boolean> adapter = moshi.adapter(boolean.class).lenient();
|
||||||
@@ -792,7 +791,8 @@ public final class MoshiTest {
|
|||||||
if (!type.equals(String.class)) return null;
|
if (!type.equals(String.class)) return null;
|
||||||
if (!Util.isAnnotationPresent(annotations, Uppercase.class)) return null;
|
if (!Util.isAnnotationPresent(annotations, Uppercase.class)) return null;
|
||||||
|
|
||||||
final JsonAdapter<String> stringAdapter = moshi.nextAdapter(this, String.class);
|
final JsonAdapter<String> stringAdapter
|
||||||
|
= moshi.nextAdapter(this, String.class, Util.NO_ANNOTATIONS);
|
||||||
return new JsonAdapter<String>() {
|
return new JsonAdapter<String>() {
|
||||||
@Override public String fromJson(JsonReader reader) throws IOException {
|
@Override public String fromJson(JsonReader reader) throws IOException {
|
||||||
String s = stringAdapter.fromJson(reader);
|
String s = stringAdapter.fromJson(reader);
|
||||||
|
171
moshi/src/test/java/com/squareup/moshi/ObjectAdapterTest.java
Normal file
171
moshi/src/test/java/com/squareup/moshi/ObjectAdapterTest.java
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2015 Square, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.squareup.moshi;
|
||||||
|
|
||||||
|
import java.util.AbstractCollection;
|
||||||
|
import java.util.AbstractList;
|
||||||
|
import java.util.AbstractMap;
|
||||||
|
import java.util.AbstractSet;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.junit.Ignore;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
public final class ObjectAdapterTest {
|
||||||
|
@Test public void toJsonUsesRuntimeType() throws Exception {
|
||||||
|
Delivery delivery = new Delivery();
|
||||||
|
delivery.address = "1455 Market St.";
|
||||||
|
Pizza pizza = new Pizza();
|
||||||
|
pizza.diameter = 12;
|
||||||
|
pizza.extraCheese = true;
|
||||||
|
delivery.items = Arrays.asList(pizza, "Pepsi");
|
||||||
|
|
||||||
|
Moshi moshi = new Moshi.Builder().build();
|
||||||
|
JsonAdapter<Object> adapter = moshi.adapter(Object.class);
|
||||||
|
assertThat(adapter.toJson(delivery)).isEqualTo("{"
|
||||||
|
+ "\"address\":\"1455 Market St.\","
|
||||||
|
+ "\"items\":["
|
||||||
|
+ "{\"diameter\":12,\"extraCheese\":true},"
|
||||||
|
+ "\"Pepsi\""
|
||||||
|
+ "]"
|
||||||
|
+ "}");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void toJsonJavaLangObject() throws Exception {
|
||||||
|
Moshi moshi = new Moshi.Builder().build();
|
||||||
|
JsonAdapter<Object> adapter = moshi.adapter(Object.class);
|
||||||
|
assertThat(adapter.toJson(new Object())).isEqualTo("{}");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void fromJsonReturnsMapsAndLists() throws Exception {
|
||||||
|
Map<Object, Object> delivery = new LinkedHashMap<>();
|
||||||
|
delivery.put("address", "1455 Market St.");
|
||||||
|
Map<Object, Object> pizza = new LinkedHashMap<>();
|
||||||
|
pizza.put("diameter", 12d);
|
||||||
|
pizza.put("extraCheese", true);
|
||||||
|
delivery.put("items", Arrays.asList(pizza, "Pepsi"));
|
||||||
|
|
||||||
|
Moshi moshi = new Moshi.Builder().build();
|
||||||
|
JsonAdapter<Object> adapter = moshi.adapter(Object.class);
|
||||||
|
assertThat(adapter.fromJson("{"
|
||||||
|
+ "\"address\":\"1455 Market St.\","
|
||||||
|
+ "\"items\":["
|
||||||
|
+ "{\"diameter\":12,\"extraCheese\":true},"
|
||||||
|
+ "\"Pepsi\""
|
||||||
|
+ "]"
|
||||||
|
+ "}")).isEqualTo(delivery);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void fromJsonUsesDoublesForNumbers() throws Exception {
|
||||||
|
Moshi moshi = new Moshi.Builder().build();
|
||||||
|
JsonAdapter<Object> adapter = moshi.adapter(Object.class);
|
||||||
|
assertThat(adapter.fromJson("[0, 1]")).isEqualTo(Arrays.asList(0d, 1d));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void toJsonCoercesRuntimeTypeForCollections() throws Exception {
|
||||||
|
Collection<String> collection = new AbstractCollection<String>() {
|
||||||
|
@Override public Iterator<String> iterator() {
|
||||||
|
return Collections.singleton("A").iterator();
|
||||||
|
}
|
||||||
|
@Override public int size() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Moshi moshi = new Moshi.Builder().build();
|
||||||
|
JsonAdapter<Object> adapter = moshi.adapter(Object.class);
|
||||||
|
assertThat(adapter.toJson(collection)).isEqualTo("[\"A\"]");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void toJsonCoercesRuntimeTypeForLists() throws Exception {
|
||||||
|
List<String> list = new AbstractList<String>() {
|
||||||
|
@Override public String get(int i) {
|
||||||
|
return "A";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public int size() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Moshi moshi = new Moshi.Builder().build();
|
||||||
|
JsonAdapter<Object> adapter = moshi.adapter(Object.class);
|
||||||
|
assertThat(adapter.toJson(list)).isEqualTo("[\"A\"]");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void toJsonCoercesRuntimeTypeForSets() throws Exception {
|
||||||
|
Set<String> set = new AbstractSet<String>() {
|
||||||
|
@Override public Iterator<String> iterator() {
|
||||||
|
return Collections.singleton("A").iterator();
|
||||||
|
}
|
||||||
|
@Override public int size() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Moshi moshi = new Moshi.Builder().build();
|
||||||
|
JsonAdapter<Object> adapter = moshi.adapter(Object.class);
|
||||||
|
assertThat(adapter.toJson(set)).isEqualTo("[\"A\"]");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore // We don't support raw maps, like Map<Object, Object>. (Even if the keys are strings!)
|
||||||
|
@Test public void toJsonCoercesRuntimeTypeForMaps() throws Exception {
|
||||||
|
Map<String, Boolean> map = new AbstractMap<String, Boolean>() {
|
||||||
|
@Override public Set<Entry<String, Boolean>> entrySet() {
|
||||||
|
return Collections.singletonMap("A", true).entrySet();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Moshi moshi = new Moshi.Builder().build();
|
||||||
|
JsonAdapter<Object> adapter = moshi.adapter(Object.class);
|
||||||
|
assertThat(adapter.toJson(map)).isEqualTo("{\"A\":true}");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void toJsonUsesTypeAdapters() throws Exception {
|
||||||
|
Object dateAdapter = new Object() {
|
||||||
|
@ToJson Long dateToJson(Date d) {
|
||||||
|
return d.getTime();
|
||||||
|
}
|
||||||
|
@FromJson Date dateFromJson(Long millis) {
|
||||||
|
return new Date(millis);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Moshi moshi = new Moshi.Builder()
|
||||||
|
.add(dateAdapter)
|
||||||
|
.build();
|
||||||
|
JsonAdapter<Object> adapter = moshi.adapter(Object.class);
|
||||||
|
assertThat(adapter.toJson(Arrays.asList(new Date(1), new Date(2)))).isEqualTo("[1,2]");
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Delivery {
|
||||||
|
String address;
|
||||||
|
List<Object> items;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Pizza {
|
||||||
|
int diameter;
|
||||||
|
boolean extraCheese;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user