Merge pull request #19 from square/jwilson_0322_classadapter

Big start into ClassAdapter.
This commit is contained in:
Jesse Wilson
2015-03-23 14:45:04 -04:00
5 changed files with 708 additions and 1 deletions

View File

@@ -0,0 +1,178 @@
/*
* 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.io.IOException;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.TreeMap;
/**
* Emits a regular class as a JSON object by mapping Java fields to JSON object properties. Fields
* of classes in {@code java.*}, {@code javax.*} and {@code android.*} are omitted from both
* serialization and deserialization unless they are either public or protected.
*/
final class ClassAdapter<T> extends JsonAdapter<T> {
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
@Override public JsonAdapter<?> create(Type type, AnnotatedElement annotations, Moshi moshi) {
Class<?> rawType = Types.getRawType(type);
if (rawType.isInterface() || rawType.isEnum() || isPlatformType(rawType)) return null;
if (rawType.getEnclosingClass() != null && !Modifier.isStatic(rawType.getModifiers())) {
if (rawType.getSimpleName().isEmpty()) {
throw new IllegalArgumentException(
"cannot serialize anonymous class " + rawType.getName());
} else {
throw new IllegalArgumentException(
"cannot serialize non-static nested class " + rawType.getName());
}
}
if (Modifier.isAbstract(rawType.getModifiers())) {
throw new IllegalArgumentException("cannot serialize abstract class " + rawType.getName());
}
ClassFactory<Object> classFactory = ClassFactory.get(rawType);
Map<String, FieldBinding<?>> fields = new TreeMap<>();
for (Type t = type; t != Object.class; t = Types.getGenericSuperclass(t)) {
createFieldBindings(moshi, t, fields);
}
return new ClassAdapter<>(classFactory, fields).nullSafe();
}
/** Creates a field binding for each of declared field of {@code type}. */
private void createFieldBindings(
Moshi moshi, Type type, Map<String, FieldBinding<?>> fieldBindings) {
Class<?> rawType = Types.getRawType(type);
boolean platformType = isPlatformType(rawType);
for (Field field : rawType.getDeclaredFields()) {
if (!includeField(platformType, field.getModifiers())) continue;
// Look up a type adapter for this type.
Type fieldType = Types.resolve(type, rawType, field.getGenericType());
JsonAdapter<Object> adapter = moshi.adapter(fieldType, field);
// Create the binding between field and JSON.
field.setAccessible(true);
FieldBinding<Object> fieldBinding = new FieldBinding<>(field, adapter);
// Store it using the field's name. If there was already a field with this name, fail!
FieldBinding<?> replaced = fieldBindings.put(field.getName(), fieldBinding);
if (replaced != null) {
throw new IllegalArgumentException("field name collision: '" + field.getName() + "'"
+ " declared by both " + replaced.field.getDeclaringClass().getName()
+ " and superclass " + fieldBinding.field.getDeclaringClass().getName());
}
}
}
/**
* Returns true if {@code rawType} is built in. We don't reflect on private fields of platform
* types because they're unspecified and likely to be different on Java vs. Android.
*/
private boolean isPlatformType(Class<?> rawType) {
return rawType.getName().startsWith("java.")
|| rawType.getName().startsWith("javax.")
|| rawType.getName().startsWith("android.");
}
/** Returns true if fields with {@code modifiers} are included in the emitted JSON. */
private boolean includeField(boolean platformType, int modifiers) {
if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers)) return false;
return Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers)|| !platformType;
}
};
private final ClassFactory<T> classFactory;
private final Map<String, FieldBinding<?>> jsonFields;
private ClassAdapter(ClassFactory<T> classFactory, Map<String, FieldBinding<?>> jsonFields) {
this.classFactory = classFactory;
this.jsonFields = jsonFields;
}
@Override public T fromJson(JsonReader reader) throws IOException {
T result;
try {
result = classFactory.newInstance();
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
Throwable targetException = e.getTargetException();
if (targetException instanceof RuntimeException) throw (RuntimeException) targetException;
if (targetException instanceof Error) throw (Error) targetException;
throw new RuntimeException(targetException);
} catch (IllegalAccessException e) {
throw new AssertionError();
}
try {
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
FieldBinding<?> fieldBinding = jsonFields.get(name);
if (fieldBinding != null) {
fieldBinding.read(reader, result);
} else {
reader.skipValue();
}
}
reader.endObject();
return result;
} catch (IllegalAccessException e) {
throw new AssertionError();
}
}
@Override public void toJson(JsonWriter writer, T value) throws IOException {
try {
writer.beginObject();
for (Map.Entry<String, FieldBinding<?>> entry : jsonFields.entrySet()) {
writer.name(entry.getKey());
entry.getValue().write(writer, value);
}
writer.endObject();
} catch (IllegalAccessException e) {
throw new AssertionError();
}
}
static class FieldBinding<T> {
private final Field field;
private final JsonAdapter<T> adapter;
public FieldBinding(Field field, JsonAdapter<T> adapter) {
this.field = field;
this.adapter = adapter;
}
private void read(JsonReader reader, Object value) throws IOException, IllegalAccessException {
T fieldValue = adapter.fromJson(reader);
field.set(value, fieldValue);
}
@SuppressWarnings("unchecked") // We require that field's values are of type T.
private void write(JsonWriter writer, Object value)
throws IllegalAccessException, IOException {
T fieldValue = (T) field.get(value);
adapter.toJson(writer, fieldValue);
}
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright (C) 2011 Google 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.io.ObjectStreamClass;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Magic that creates instances of arbitrary concrete classes. Derived from Gson's UnsafeAllocator
* and ConstructorConstructor classes.
*
* @author Joel Leitch
* @author Jesse Wilson
*/
abstract class ClassFactory<T> {
abstract T newInstance() throws
InvocationTargetException, IllegalAccessException, InstantiationException;
public static <T> ClassFactory<T> get(final Class<?> rawType) {
// Try to find a no-args constructor. May be any visibility including private.
try {
final Constructor<?> constructor = rawType.getDeclaredConstructor();
constructor.setAccessible(true);
return new ClassFactory<T>() {
@SuppressWarnings("unchecked") // T is the same raw type as is requested
@Override public T newInstance() throws IllegalAccessException, InvocationTargetException,
InstantiationException {
Object[] args = null;
return (T) constructor.newInstance(args);
}
};
} catch (NoSuchMethodException noNoArgsConstructor) {
// No no-args constructor. Fall back to something more magical...
}
// Try the JVM's Unsafe mechanism.
// public class Unsafe {
// public Object allocateInstance(Class<?> type);
// }
try {
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
Field f = unsafeClass.getDeclaredField("theUnsafe");
f.setAccessible(true);
final Object unsafe = f.get(null);
final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class);
return new ClassFactory<T>() {
@SuppressWarnings("unchecked")
@Override public T newInstance() throws InvocationTargetException, IllegalAccessException {
return (T) allocateInstance.invoke(unsafe, rawType);
}
};
} catch (IllegalAccessException e) {
throw new AssertionError();
} catch (ClassNotFoundException | NoSuchMethodException | NoSuchFieldException notJvm) {
// Not the expected version of the Oracle Java library!
}
// Try Dalvik/libcore's ObjectStreamClass mechanism.
// public class ObjectStreamClass {
// private static native int getConstructorId(Class<?> c);
// private static native Object newInstance(Class<?> instantiationClass, int methodId);
// }
try {
Method getConstructorId = ObjectStreamClass.class.getDeclaredMethod(
"getConstructorId", Class.class);
getConstructorId.setAccessible(true);
final int constructorId = (Integer) getConstructorId.invoke(null, Object.class);
final Method newInstance = ObjectStreamClass.class.getDeclaredMethod("newInstance",
Class.class, int.class);
newInstance.setAccessible(true);
return new ClassFactory<T>() {
@SuppressWarnings("unchecked")
@Override public T newInstance() throws InvocationTargetException, IllegalAccessException {
return (T) newInstance.invoke(null, rawType, constructorId);
}
};
} catch (IllegalAccessException e) {
throw new AssertionError();
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException notLibcore) {
// Not the expected version of Dalvik/libcore!
}
throw new IllegalArgumentException("cannot construct instances of " + rawType.getName());
}
}

View File

@@ -254,6 +254,11 @@ final class Types {
getGenericSupertype(context, contextRawType, supertype)); getGenericSupertype(context, contextRawType, supertype));
} }
public static Type getGenericSuperclass(Type type) {
Class<?> rawType = Types.getRawType(type);
return resolve(type, rawType, rawType.getGenericSuperclass());
}
/** /**
* Returns the element type of {@code type} if it is an array type, or null if it is not an * Returns the element type of {@code type} if it is an array type, or null if it is not an
* array type. * array type.

View File

@@ -0,0 +1,420 @@
/*
* 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.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Comparator;
import java.util.SimpleTimeZone;
import java.util.UUID;
import javax.crypto.KeyGenerator;
import okio.Buffer;
import org.junit.Test;
import static com.squareup.moshi.Util.NO_ANNOTATIONS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
public final class ClassAdapterTest {
private final Moshi moshi = new Moshi.Builder().build();
static class BasicPizza {
int diameter;
boolean extraCheese;
}
@Test public void basicClassAdapter() throws Exception {
BasicPizza value = new BasicPizza();
value.diameter = 13;
value.extraCheese = true;
String toJson = toJson(BasicPizza.class, value);
assertThat(toJson).isEqualTo("{\"diameter\":13,\"extraCheese\":true}");
BasicPizza fromJson = fromJson(BasicPizza.class, "{\"diameter\":13,\"extraCheese\":true}");
assertThat(fromJson.diameter).isEqualTo(13);
assertThat(fromJson.extraCheese).isTrue();
}
static class PrivateFieldsPizza {
private String secretIngredient;
}
@Test public void privateFields() throws Exception {
PrivateFieldsPizza value = new PrivateFieldsPizza();
value.secretIngredient = "vodka";
String toJson = toJson(PrivateFieldsPizza.class, value);
assertThat(toJson).isEqualTo("{\"secretIngredient\":\"vodka\"}");
PrivateFieldsPizza fromJson = fromJson(
PrivateFieldsPizza.class, "{\"secretIngredient\":\"vodka\"}");
assertThat(fromJson.secretIngredient).isEqualTo("vodka");
}
static class BasePizza {
int diameter;
}
static class DessertPizza extends BasePizza {
boolean chocolate;
}
@Test public void typeHierarchy() throws Exception {
DessertPizza value = new DessertPizza();
value.diameter = 13;
value.chocolate = true;
String toJson = toJson(DessertPizza.class, value);
assertThat(toJson).isEqualTo("{\"chocolate\":true,\"diameter\":13}");
DessertPizza fromJson = fromJson(DessertPizza.class, "{\"diameter\":13,\"chocolate\":true}");
assertThat(fromJson.diameter).isEqualTo(13);
assertThat(fromJson.chocolate).isTrue();
}
static class BaseAbcde {
int d;
int a;
int c;
}
static class ExtendsBaseAbcde extends BaseAbcde {
int b;
int e;
}
@Test public void fieldsAreAlphabeticalAcrossFlattenedHierarchy() throws Exception {
ExtendsBaseAbcde value = new ExtendsBaseAbcde();
value.a = 4;
value.b = 5;
value.c = 6;
value.d = 7;
value.e = 8;
String toJson = toJson(ExtendsBaseAbcde.class, value);
assertThat(toJson).isEqualTo("{\"a\":4,\"b\":5,\"c\":6,\"d\":7,\"e\":8}");
ExtendsBaseAbcde fromJson = fromJson(
ExtendsBaseAbcde.class, "{\"a\":4,\"b\":5,\"c\":6,\"d\":7,\"e\":8}");
assertThat(fromJson.a).isEqualTo(4);
assertThat(fromJson.b).isEqualTo(5);
assertThat(fromJson.c).isEqualTo(6);
assertThat(fromJson.d).isEqualTo(7);
assertThat(fromJson.e).isEqualTo(8);
}
static class StaticFields {
static int a = 11;
int b;
}
@Test public void staticFieldsOmitted() throws Exception {
StaticFields value = new StaticFields();
value.b = 12;
String toJson = toJson(StaticFields.class, value);
assertThat(toJson).isEqualTo("{\"b\":12}");
StaticFields fromJson = fromJson(StaticFields.class, "{\"a\":13,\"b\":12}");
assertThat(StaticFields.a).isEqualTo(11); // Unchanged.
assertThat(fromJson.b).isEqualTo(12);
}
static class TransientFields {
transient int a;
int b;
}
@Test public void transientFieldsOmitted() throws Exception {
TransientFields value = new TransientFields();
value.a = 11;
value.b = 12;
String toJson = toJson(TransientFields.class, value);
assertThat(toJson).isEqualTo("{\"b\":12}");
TransientFields fromJson = fromJson(TransientFields.class, "{\"a\":13,\"b\":12}");
assertThat(fromJson.a).isEqualTo(0); // Not assigned.
assertThat(fromJson.b).isEqualTo(12);
}
static class BaseA {
int a;
}
static class ExtendsBaseA extends BaseA {
int a;
}
@Test public void fieldNameCollision() throws Exception {
try {
ClassAdapter.FACTORY.create(ExtendsBaseA.class, NO_ANNOTATIONS, moshi);
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("field name collision: 'a' declared by both "
+ "com.squareup.moshi.ClassAdapterTest$ExtendsBaseA and "
+ "superclass com.squareup.moshi.ClassAdapterTest$BaseA");
}
}
static class TransientBaseA {
transient int a;
}
static class ExtendsTransientBaseA extends TransientBaseA {
int a;
}
@Test public void fieldNameCollisionWithTransientFieldIsOkay() throws Exception {
ExtendsTransientBaseA value = new ExtendsTransientBaseA();
value.a = 11;
((TransientBaseA) value).a = 12;
String toJson = toJson(ExtendsTransientBaseA.class, value);
assertThat(toJson).isEqualTo("{\"a\":11}");
ExtendsTransientBaseA fromJson = fromJson(ExtendsTransientBaseA.class, "{\"a\":11}");
assertThat(fromJson.a).isEqualTo(11);
assertThat(((TransientBaseA) fromJson).a).isEqualTo(0); // Not assigned.
}
static class NoArgConstructor {
int a;
int b;
NoArgConstructor() {
a = 5;
}
}
@Test public void noArgConstructor() throws Exception {
NoArgConstructor fromJson = fromJson(NoArgConstructor.class, "{\"b\":8}");
assertThat(fromJson.a).isEqualTo(5);
assertThat(fromJson.b).isEqualTo(8);
}
static class NoArgConstructorThrowsCheckedException {
NoArgConstructorThrowsCheckedException() throws Exception {
throw new Exception("foo");
}
}
@Test public void noArgConstructorThrowsCheckedException() throws Exception {
try {
fromJson(NoArgConstructorThrowsCheckedException.class, "{}");
fail();
} catch (RuntimeException expected) {
assertThat(expected.getCause()).hasMessage("foo");
}
}
static class NoArgConstructorThrowsUncheckedException {
NoArgConstructorThrowsUncheckedException() throws Exception {
throw new UnsupportedOperationException("foo");
}
}
@Test public void noArgConstructorThrowsUncheckedException() throws Exception {
try {
fromJson(NoArgConstructorThrowsUncheckedException.class, "{}");
fail();
} catch (UnsupportedOperationException expected) {
assertThat(expected).hasMessage("foo");
}
}
static class NoArgConstructorWithDefaultField {
int a = 5;
int b;
}
@Test public void noArgConstructorFieldDefaultsHonored() throws Exception {
NoArgConstructorWithDefaultField fromJson = fromJson(
NoArgConstructorWithDefaultField.class, "{\"b\":8}");
assertThat(fromJson.a).isEqualTo(5);
assertThat(fromJson.b).isEqualTo(8);
}
static class MagicConstructor {
int a;
public MagicConstructor(Void argument) {
throw new AssertionError();
}
}
@Test public void magicConstructor() throws Exception {
MagicConstructor fromJson = fromJson(MagicConstructor.class, "{\"a\":8}");
assertThat(fromJson.a).isEqualTo(8);
}
static class MagicConstructorWithDefaultField {
int a = 5;
int b;
public MagicConstructorWithDefaultField(Void argument) {
throw new AssertionError();
}
}
@Test public void magicConstructorFieldDefaultsNotHonored() throws Exception {
MagicConstructorWithDefaultField fromJson = fromJson(
MagicConstructorWithDefaultField.class, "{\"b\":3}");
assertThat(fromJson.a).isEqualTo(0); // Surprising! No value is assigned.
assertThat(fromJson.b).isEqualTo(3);
}
static class NullRootObject {
int a;
}
@Test public void nullRootObject() throws Exception {
String toJson = toJson(PrivateFieldsPizza.class, null);
assertThat(toJson).isEqualTo("null");
NullRootObject fromJson = fromJson(NullRootObject.class, "null");
assertThat(fromJson).isNull();
}
static class NullFieldValue {
String a = "not null";
}
@Test public void nullFieldValues() throws Exception {
NullFieldValue value = new NullFieldValue();
value.a = null;
String toJson = toJson(NullFieldValue.class, value);
assertThat(toJson).isEqualTo("{\"a\":null}");
NullFieldValue fromJson = fromJson(NullFieldValue.class, "{\"a\":null}");
assertThat(fromJson.a).isNull();
}
class NonStatic {
}
@Test public void nonStaticNestedClassNotSupported() throws Exception {
try {
ClassAdapter.FACTORY.create(NonStatic.class, NO_ANNOTATIONS, moshi);
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("cannot serialize non-static nested class "
+ "com.squareup.moshi.ClassAdapterTest$NonStatic");
}
}
@Test public void platformClassNotSupported() throws Exception {
assertThat(ClassAdapter.FACTORY.create(UUID.class, NO_ANNOTATIONS, moshi)).isNull();
assertThat(ClassAdapter.FACTORY.create(KeyGenerator.class, NO_ANNOTATIONS, moshi)).isNull();
}
@Test public void anonymousClassNotSupported() throws Exception {
Comparator<Object> c = new Comparator<Object>() {
@Override public int compare(Object a, Object b) {
return 0;
}
};
try {
ClassAdapter.FACTORY.create(c.getClass(), NO_ANNOTATIONS, moshi);
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("cannot serialize anonymous class " + c.getClass().getName());
}
}
@Test public void interfaceNotSupported() throws Exception {
assertThat(ClassAdapter.FACTORY.create(Runnable.class, NO_ANNOTATIONS, moshi)).isNull();
}
static abstract class Abstract {
}
@Test public void abstractClassNotSupported() throws Exception {
try {
ClassAdapter.FACTORY.create(Abstract.class, NO_ANNOTATIONS, moshi);
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("cannot serialize abstract class "
+ "com.squareup.moshi.ClassAdapterTest$Abstract");
}
}
static class ExtendsPlatformClassWithPrivateField extends SimpleTimeZone {
int a;
public ExtendsPlatformClassWithPrivateField() {
super(0, "FOO");
}
}
@Test public void platformSuperclassPrivateFieldIsExcluded() throws Exception {
ExtendsPlatformClassWithPrivateField value = new ExtendsPlatformClassWithPrivateField();
value.a = 4;
String toJson = toJson(ExtendsPlatformClassWithPrivateField.class, value);
assertThat(toJson).isEqualTo("{\"a\":4}");
ExtendsPlatformClassWithPrivateField fromJson = fromJson(
ExtendsPlatformClassWithPrivateField.class, "{\"a\":4,\"ID\":\"BAR\"}");
assertThat(fromJson.a).isEqualTo(4);
assertThat(fromJson.getID()).isEqualTo("FOO");
}
static class ExtendsPlatformClassWithProtectedField extends ByteArrayOutputStream {
int a;
public ExtendsPlatformClassWithProtectedField() {
super(2);
}
}
@Test public void platformSuperclassProtectedFieldIsIncluded() throws Exception {
ExtendsPlatformClassWithProtectedField value = new ExtendsPlatformClassWithProtectedField();
value.a = 4;
value.write(5);
value.write(6);
String toJson = toJson(ExtendsPlatformClassWithProtectedField.class, value);
assertThat(toJson).isEqualTo("{\"a\":4,\"buf\":[5,6],\"count\":2}");
ExtendsPlatformClassWithProtectedField fromJson = fromJson(
ExtendsPlatformClassWithProtectedField.class, "{\"a\":4,\"buf\":[5,6],\"count\":2}");
assertThat(fromJson.a).isEqualTo(4);
assertThat(fromJson.toByteArray()).contains((byte) 5, (byte) 6);
}
private <T> String toJson(Class<T> type, T value) throws IOException {
@SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument.
JsonAdapter<T> jsonAdapter = (JsonAdapter<T>) ClassAdapter.FACTORY.create(
type, NO_ANNOTATIONS, moshi);
// Wrap in an array to avoid top-level object warnings without going completely lenient.
Buffer buffer = new Buffer();
JsonWriter jsonWriter = new JsonWriter(buffer);
jsonWriter.setSerializeNulls(true);
jsonWriter.beginArray();
jsonAdapter.toJson(jsonWriter, value);
jsonWriter.endArray();
assertThat(buffer.readByte()).isEqualTo((byte) '[');
String json = buffer.readUtf8(buffer.size() - 1);
assertThat(buffer.readByte()).isEqualTo((byte) ']');
return json;
}
private <T> T fromJson(Class<T> type, String json) throws IOException {
@SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument.
JsonAdapter<T> jsonAdapter = (JsonAdapter<T>) ClassAdapter.FACTORY.create(
type, NO_ANNOTATIONS, moshi);
// Wrap in an array to avoid top-level object warnings without going completely lenient.
JsonReader jsonReader = new JsonReader("[" + json + "]");
jsonReader.beginArray();
T result = jsonAdapter.fromJson(jsonReader);
jsonReader.endArray();
return result;
}
}

View File

@@ -23,7 +23,7 @@
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.6</java.version> <java.version>1.7</java.version>
<!-- Dependencies --> <!-- Dependencies -->
<okio.version>1.1.0</okio.version> <okio.version>1.1.0</okio.version>