Merge pull request #52 from square/jwilson_0613_object_adapter

Runtime type adapter.
This commit is contained in:
Jake Wharton
2015-06-14 17:51:49 -04:00
8 changed files with 260 additions and 28 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
});

View File

@@ -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;
}
}
}

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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);

View 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;
}
}