mirror of
https://github.com/fankes/moshi.git
synced 2025-10-18 23:49:21 +08:00
Big start into ClassAdapter.
This borrows from Gson's UnsafeAllocator. I didn't actually borrow much from Gson's reflective type adapter, but I'll need to review that in follow up to see if I forgot anything that Gson covers. Most interesting design decision here is that fields are serialized in alphabetical order. Also we're pretty nice around detecting field collisiosn and failing early.
This commit is contained in:
178
moshi/src/main/java/com/squareup/moshi/ClassAdapter.java
Normal file
178
moshi/src/main/java/com/squareup/moshi/ClassAdapter.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
104
moshi/src/main/java/com/squareup/moshi/ClassFactory.java
Normal file
104
moshi/src/main/java/com/squareup/moshi/ClassFactory.java
Normal 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());
|
||||
}
|
||||
}
|
@@ -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.
|
||||
|
420
moshi/src/test/java/com/squareup/moshi/ClassAdapterTest.java
Normal file
420
moshi/src/test/java/com/squareup/moshi/ClassAdapterTest.java
Normal 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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user