diff --git a/moshi/src/main/java/com/squareup/moshi/ClassAdapter.java b/moshi/src/main/java/com/squareup/moshi/ClassAdapter.java new file mode 100644 index 0000000..3293674 --- /dev/null +++ b/moshi/src/main/java/com/squareup/moshi/ClassAdapter.java @@ -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 extends JsonAdapter { + 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 classFactory = ClassFactory.get(rawType); + Map> 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> 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 adapter = moshi.adapter(fieldType, field); + + // Create the binding between field and JSON. + field.setAccessible(true); + FieldBinding 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 classFactory; + private final Map> jsonFields; + + private ClassAdapter(ClassFactory classFactory, Map> 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> entry : jsonFields.entrySet()) { + writer.name(entry.getKey()); + entry.getValue().write(writer, value); + } + writer.endObject(); + } catch (IllegalAccessException e) { + throw new AssertionError(); + } + } + + static class FieldBinding { + private final Field field; + private final JsonAdapter adapter; + + public FieldBinding(Field field, JsonAdapter 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); + } + } +} diff --git a/moshi/src/main/java/com/squareup/moshi/ClassFactory.java b/moshi/src/main/java/com/squareup/moshi/ClassFactory.java new file mode 100644 index 0000000..3f55581 --- /dev/null +++ b/moshi/src/main/java/com/squareup/moshi/ClassFactory.java @@ -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 { + abstract T newInstance() throws + InvocationTargetException, IllegalAccessException, InstantiationException; + + public static ClassFactory 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() { + @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() { + @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() { + @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()); + } +} diff --git a/moshi/src/main/java/com/squareup/moshi/Types.java b/moshi/src/main/java/com/squareup/moshi/Types.java index 0b3dc85..7f3fe71 100644 --- a/moshi/src/main/java/com/squareup/moshi/Types.java +++ b/moshi/src/main/java/com/squareup/moshi/Types.java @@ -254,6 +254,11 @@ final class Types { 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 * array type. diff --git a/moshi/src/test/java/com/squareup/moshi/ClassAdapterTest.java b/moshi/src/test/java/com/squareup/moshi/ClassAdapterTest.java new file mode 100644 index 0000000..f929a03 --- /dev/null +++ b/moshi/src/test/java/com/squareup/moshi/ClassAdapterTest.java @@ -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 c = new Comparator() { + @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 String toJson(Class type, T value) throws IOException { + @SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument. + JsonAdapter jsonAdapter = (JsonAdapter) 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 fromJson(Class type, String json) throws IOException { + @SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument. + JsonAdapter jsonAdapter = (JsonAdapter) 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; + } +} diff --git a/pom.xml b/pom.xml index 26d81f8..85059c7 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ UTF-8 - 1.6 + 1.7 1.1.0