mirror of
https://github.com/fankes/moshi.git
synced 2025-10-19 16:09: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.Set;
|
||||
|
||||
// TODO: support @Nullable
|
||||
// TODO: path in JsonWriter.
|
||||
|
||||
final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
||||
private final List<AdapterMethod> toAdapters;
|
||||
private final List<AdapterMethod> fromAdapters;
|
||||
|
@@ -1198,11 +1198,8 @@ public final class JsonReader implements Closeable {
|
||||
return c;
|
||||
}
|
||||
} else if (c == '#') {
|
||||
/*
|
||||
* Skip a # hash end-of-line comment. The JSON RFC doesn't
|
||||
* specify this behaviour, but it's required to parse
|
||||
* existing documents. See http://b/2571423.
|
||||
*/
|
||||
// Skip a # hash end-of-line comment. The JSON RFC doesn't specify this behaviour, but it's
|
||||
// required to parse existing documents. See http://b/2571423.
|
||||
checkLenient();
|
||||
skipToEndOfLine();
|
||||
p = 0;
|
||||
|
@@ -41,11 +41,11 @@ public final class Moshi {
|
||||
this.factories = Collections.unmodifiableList(factories);
|
||||
}
|
||||
|
||||
/** Returns a JSON adapter for {@code type}, creating it if necessary. */
|
||||
public <T> JsonAdapter<T> adapter(Type type) {
|
||||
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) {
|
||||
// TODO: cache created JSON adapters.
|
||||
return adapter(type, Util.NO_ANNOTATIONS);
|
||||
@@ -60,10 +60,6 @@ public final class Moshi {
|
||||
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.
|
||||
private <T> JsonAdapter<T> createAdapter(
|
||||
int firstIndex, Type type, Set<? extends Annotation> annotations) {
|
||||
@@ -125,7 +121,10 @@ public final class Moshi {
|
||||
@Override public JsonAdapter<?> create(
|
||||
Type targetType, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
if (!Util.typesMatch(type, targetType)) return null;
|
||||
|
||||
// TODO: check for an annotations exact match.
|
||||
if (!Util.isAnnotationPresent(annotations, annotation)) return null;
|
||||
|
||||
return jsonAdapter;
|
||||
}
|
||||
});
|
||||
|
@@ -18,7 +18,11 @@ package com.squareup.moshi;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
final class StandardJsonAdapters {
|
||||
@@ -43,6 +47,7 @@ final class StandardJsonAdapters {
|
||||
if (type == Long.class) return LONG_JSON_ADAPTER.nullSafe();
|
||||
if (type == Short.class) return SHORT_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);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}. */
|
||||
public static GenericArrayType arrayOf(Type componentType) {
|
||||
return new GenericArrayTypeImpl(componentType);
|
||||
|
@@ -113,7 +113,7 @@ public final class CircularAdaptersTest {
|
||||
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>() {
|
||||
@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;
|
||||
|
||||
public final class MoshiTest {
|
||||
|
||||
@Test public void booleanAdapter() throws Exception {
|
||||
Moshi moshi = new Moshi.Builder().build();
|
||||
JsonAdapter<Boolean> adapter = moshi.adapter(boolean.class).lenient();
|
||||
@@ -792,7 +791,8 @@ public final class MoshiTest {
|
||||
if (!type.equals(String.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>() {
|
||||
@Override public String fromJson(JsonReader reader) throws IOException {
|
||||
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