mirror of
https://github.com/fankes/moshi.git
synced 2025-10-19 07:59:21 +08:00
Merge pull request #19 from square/jwilson_0322_classadapter
Big start into ClassAdapter.
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