Fix `Gson.getDelegateAdapter` not working properly for `JsonAdapter` (#2435)
* Fix `Gson.getDelegateAdapter` not working properly for `JsonAdapter` * Address review feedback and add comments regarding thread-safety * Revert InstanceCreator instance validation * Disallow `null` as `skipPast` * Avoid `equals` usage in `getDelegateAdapter` & minor other changes Previously `getDelegateAdapter` called `factories.contains(skipPast)`, but unlike the other comparisons which check for reference equality, that would have used the `equals` method. This could lead to spurious "GSON cannot serialize ..." exceptions if two factory instances compared equal, but the one provided as `skipPast` had not been registered yet.
This commit is contained in:
parent
393db094dd
commit
7ee5ad6cd1
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package com.google.gson;
|
package com.google.gson;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.JsonAdapter;
|
||||||
import com.google.gson.internal.ConstructorConstructor;
|
import com.google.gson.internal.ConstructorConstructor;
|
||||||
import com.google.gson.internal.Excluder;
|
import com.google.gson.internal.Excluder;
|
||||||
import com.google.gson.internal.GsonBuildConfig;
|
import com.google.gson.internal.GsonBuildConfig;
|
||||||
|
@ -604,42 +605,50 @@ public final class Gson {
|
||||||
* adapter that does a little bit of work but then delegates further processing to the Gson
|
* adapter that does a little bit of work but then delegates further processing to the Gson
|
||||||
* default type adapter. Here is an example:
|
* default type adapter. Here is an example:
|
||||||
* <p>Let's say we want to write a type adapter that counts the number of objects being read
|
* <p>Let's say we want to write a type adapter that counts the number of objects being read
|
||||||
* from or written to JSON. We can achieve this by writing a type adapter factory that uses
|
* from or written to JSON. We can achieve this by writing a type adapter factory that uses
|
||||||
* the <code>getDelegateAdapter</code> method:
|
* the <code>getDelegateAdapter</code> method:
|
||||||
* <pre> {@code
|
* <pre>{@code
|
||||||
* class StatsTypeAdapterFactory implements TypeAdapterFactory {
|
* class StatsTypeAdapterFactory implements TypeAdapterFactory {
|
||||||
* public int numReads = 0;
|
* public int numReads = 0;
|
||||||
* public int numWrites = 0;
|
* public int numWrites = 0;
|
||||||
* public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
|
* public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
|
||||||
* final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
|
* final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
|
||||||
* return new TypeAdapter<T>() {
|
* return new TypeAdapter<T>() {
|
||||||
* public void write(JsonWriter out, T value) throws IOException {
|
* public void write(JsonWriter out, T value) throws IOException {
|
||||||
* ++numWrites;
|
* ++numWrites;
|
||||||
* delegate.write(out, value);
|
* delegate.write(out, value);
|
||||||
* }
|
* }
|
||||||
* public T read(JsonReader in) throws IOException {
|
* public T read(JsonReader in) throws IOException {
|
||||||
* ++numReads;
|
* ++numReads;
|
||||||
* return delegate.read(in);
|
* return delegate.read(in);
|
||||||
* }
|
* }
|
||||||
* };
|
* };
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
* } </pre>
|
* }</pre>
|
||||||
* This factory can now be used like this:
|
* This factory can now be used like this:
|
||||||
* <pre> {@code
|
* <pre>{@code
|
||||||
* StatsTypeAdapterFactory stats = new StatsTypeAdapterFactory();
|
* StatsTypeAdapterFactory stats = new StatsTypeAdapterFactory();
|
||||||
* Gson gson = new GsonBuilder().registerTypeAdapterFactory(stats).create();
|
* Gson gson = new GsonBuilder().registerTypeAdapterFactory(stats).create();
|
||||||
* // Call gson.toJson() and fromJson methods on objects
|
* // Call gson.toJson() and fromJson methods on objects
|
||||||
* System.out.println("Num JSON reads" + stats.numReads);
|
* System.out.println("Num JSON reads: " + stats.numReads);
|
||||||
* System.out.println("Num JSON writes" + stats.numWrites);
|
* System.out.println("Num JSON writes: " + stats.numWrites);
|
||||||
* }</pre>
|
* }</pre>
|
||||||
* Note that this call will skip all factories registered before {@code skipPast}. In case of
|
* Note that this call will skip all factories registered before {@code skipPast}. In case of
|
||||||
* multiple TypeAdapterFactories registered it is up to the caller of this function to insure
|
* multiple TypeAdapterFactories registered it is up to the caller of this function to insure
|
||||||
* that the order of registration does not prevent this method from reaching a factory they
|
* that the order of registration does not prevent this method from reaching a factory they
|
||||||
* would expect to reply from this call.
|
* would expect to reply from this call.
|
||||||
* Note that since you can not override type adapter factories for String and Java primitive
|
* Note that since you can not override the type adapter factories for some types, see
|
||||||
* types, our stats factory will not count the number of String or primitives that will be
|
* {@link GsonBuilder#registerTypeAdapter(Type, Object)}, our stats factory will not count
|
||||||
* read or written.
|
* the number of instances of those types that will be read or written.
|
||||||
|
*
|
||||||
|
* <p>If {@code skipPast} is a factory which has neither been registered on the {@link GsonBuilder}
|
||||||
|
* nor specified with the {@link JsonAdapter @JsonAdapter} annotation on a class, then this
|
||||||
|
* method behaves as if {@link #getAdapter(TypeToken)} had been called. This also means that
|
||||||
|
* for fields with {@code @JsonAdapter} annotation this method behaves normally like {@code getAdapter}
|
||||||
|
* (except for corner cases where a custom {@link InstanceCreator} is used to create an
|
||||||
|
* instance of the factory).
|
||||||
|
*
|
||||||
* @param skipPast The type adapter factory that needs to be skipped while searching for
|
* @param skipPast The type adapter factory that needs to be skipped while searching for
|
||||||
* a matching type adapter. In most cases, you should just pass <i>this</i> (the type adapter
|
* a matching type adapter. In most cases, you should just pass <i>this</i> (the type adapter
|
||||||
* factory from where {@code getDelegateAdapter} method is being invoked).
|
* factory from where {@code getDelegateAdapter} method is being invoked).
|
||||||
|
@ -648,9 +657,10 @@ public final class Gson {
|
||||||
* @since 2.2
|
* @since 2.2
|
||||||
*/
|
*/
|
||||||
public <T> TypeAdapter<T> getDelegateAdapter(TypeAdapterFactory skipPast, TypeToken<T> type) {
|
public <T> TypeAdapter<T> getDelegateAdapter(TypeAdapterFactory skipPast, TypeToken<T> type) {
|
||||||
// Hack. If the skipPast factory isn't registered, assume the factory is being requested via
|
Objects.requireNonNull(skipPast, "skipPast must not be null");
|
||||||
// our @JsonAdapter annotation.
|
Objects.requireNonNull(type, "type must not be null");
|
||||||
if (!factories.contains(skipPast)) {
|
|
||||||
|
if (jsonAdapterFactory.isClassJsonAdapterFactory(type, skipPast)) {
|
||||||
skipPast = jsonAdapterFactory;
|
skipPast = jsonAdapterFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -668,7 +678,13 @@ public final class Gson {
|
||||||
return candidate;
|
return candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new IllegalArgumentException("GSON cannot serialize " + type);
|
|
||||||
|
if (skipPastFound) {
|
||||||
|
throw new IllegalArgumentException("GSON cannot serialize " + type);
|
||||||
|
} else {
|
||||||
|
// Probably a factory from @JsonAdapter on a field
|
||||||
|
return getAdapter(type);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -63,7 +63,7 @@ import java.lang.reflect.Type;
|
||||||
* </pre>
|
* </pre>
|
||||||
*
|
*
|
||||||
* <p>Note that it does not matter what the fields of the created instance contain since Gson will
|
* <p>Note that it does not matter what the fields of the created instance contain since Gson will
|
||||||
* overwrite them with the deserialized values specified in Json. You should also ensure that a
|
* overwrite them with the deserialized values specified in JSON. You should also ensure that a
|
||||||
* <i>new</i> object is returned, not a common object since its fields will be overwritten.
|
* <i>new</i> object is returned, not a common object since its fields will be overwritten.
|
||||||
* The developer will need to register {@code IdInstanceCreator} with Gson as follows:</p>
|
* The developer will need to register {@code IdInstanceCreator} with Gson as follows:</p>
|
||||||
*
|
*
|
||||||
|
@ -81,7 +81,7 @@ public interface InstanceCreator<T> {
|
||||||
/**
|
/**
|
||||||
* Gson invokes this call-back method during deserialization to create an instance of the
|
* Gson invokes this call-back method during deserialization to create an instance of the
|
||||||
* specified type. The fields of the returned instance are overwritten with the data present
|
* specified type. The fields of the returned instance are overwritten with the data present
|
||||||
* in the Json. Since the prior contents of the object are destroyed and overwritten, do not
|
* in the JSON. Since the prior contents of the object are destroyed and overwritten, do not
|
||||||
* return an instance that is useful elsewhere. In particular, do not return a common instance,
|
* return an instance that is useful elsewhere. In particular, do not return a common instance,
|
||||||
* always use {@code new} to create a new instance.
|
* always use {@code new} to create a new instance.
|
||||||
*
|
*
|
||||||
|
|
|
@ -24,6 +24,9 @@ import com.google.gson.TypeAdapterFactory;
|
||||||
import com.google.gson.annotations.JsonAdapter;
|
import com.google.gson.annotations.JsonAdapter;
|
||||||
import com.google.gson.internal.ConstructorConstructor;
|
import com.google.gson.internal.ConstructorConstructor;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a type T, looks for the annotation {@link JsonAdapter} and uses an instance of the
|
* Given a type T, looks for the annotation {@link JsonAdapter} and uses an instance of the
|
||||||
|
@ -32,35 +35,85 @@ import com.google.gson.reflect.TypeToken;
|
||||||
* @since 2.3
|
* @since 2.3
|
||||||
*/
|
*/
|
||||||
public final class JsonAdapterAnnotationTypeAdapterFactory implements TypeAdapterFactory {
|
public final class JsonAdapterAnnotationTypeAdapterFactory implements TypeAdapterFactory {
|
||||||
|
private static class DummyTypeAdapterFactory implements TypeAdapterFactory {
|
||||||
|
@Override public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
|
||||||
|
throw new AssertionError("Factory should not be used");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory used for {@link TreeTypeAdapter}s created for {@code @JsonAdapter}
|
||||||
|
* on a class.
|
||||||
|
*/
|
||||||
|
private static final TypeAdapterFactory TREE_TYPE_CLASS_DUMMY_FACTORY = new DummyTypeAdapterFactory();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory used for {@link TreeTypeAdapter}s created for {@code @JsonAdapter}
|
||||||
|
* on a field.
|
||||||
|
*/
|
||||||
|
private static final TypeAdapterFactory TREE_TYPE_FIELD_DUMMY_FACTORY = new DummyTypeAdapterFactory();
|
||||||
|
|
||||||
private final ConstructorConstructor constructorConstructor;
|
private final ConstructorConstructor constructorConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For a class, if it is annotated with {@code @JsonAdapter} and refers to a {@link TypeAdapterFactory},
|
||||||
|
* stores the factory instance in case it has been requested already.
|
||||||
|
* Has to be a {@link ConcurrentMap} because {@link Gson} guarantees to be thread-safe.
|
||||||
|
*/
|
||||||
|
// Note: In case these strong reference to TypeAdapterFactory instances are considered
|
||||||
|
// a memory leak in the future, could consider switching to WeakReference<TypeAdapterFactory>
|
||||||
|
private final ConcurrentMap<Class<?>, TypeAdapterFactory> adapterFactoryMap;
|
||||||
|
|
||||||
public JsonAdapterAnnotationTypeAdapterFactory(ConstructorConstructor constructorConstructor) {
|
public JsonAdapterAnnotationTypeAdapterFactory(ConstructorConstructor constructorConstructor) {
|
||||||
this.constructorConstructor = constructorConstructor;
|
this.constructorConstructor = constructorConstructor;
|
||||||
|
this.adapterFactoryMap = new ConcurrentHashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separate helper method to make sure callers retrieve annotation in a consistent way
|
||||||
|
private JsonAdapter getAnnotation(Class<?> rawType) {
|
||||||
|
return rawType.getAnnotation(JsonAdapter.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked") // this is not safe; requires that user has specified correct adapter class for @JsonAdapter
|
@SuppressWarnings("unchecked") // this is not safe; requires that user has specified correct adapter class for @JsonAdapter
|
||||||
@Override
|
@Override
|
||||||
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> targetType) {
|
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> targetType) {
|
||||||
Class<? super T> rawType = targetType.getRawType();
|
Class<? super T> rawType = targetType.getRawType();
|
||||||
JsonAdapter annotation = rawType.getAnnotation(JsonAdapter.class);
|
JsonAdapter annotation = getAnnotation(rawType);
|
||||||
if (annotation == null) {
|
if (annotation == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (TypeAdapter<T>) getTypeAdapter(constructorConstructor, gson, targetType, annotation);
|
return (TypeAdapter<T>) getTypeAdapter(constructorConstructor, gson, targetType, annotation, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separate helper method to make sure callers create adapter in a consistent way
|
||||||
|
private static Object createAdapter(ConstructorConstructor constructorConstructor, Class<?> adapterClass) {
|
||||||
|
// TODO: The exception messages created by ConstructorConstructor are currently written in the context of
|
||||||
|
// deserialization and for example suggest usage of TypeAdapter, which would not work for @JsonAdapter usage
|
||||||
|
return constructorConstructor.get(TypeToken.get(adapterClass)).construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
private TypeAdapterFactory putFactoryAndGetCurrent(Class<?> rawType, TypeAdapterFactory factory) {
|
||||||
|
// Uses putIfAbsent in case multiple threads concurrently create factory
|
||||||
|
TypeAdapterFactory existingFactory = adapterFactoryMap.putIfAbsent(rawType, factory);
|
||||||
|
return existingFactory != null ? existingFactory : factory;
|
||||||
}
|
}
|
||||||
|
|
||||||
TypeAdapter<?> getTypeAdapter(ConstructorConstructor constructorConstructor, Gson gson,
|
TypeAdapter<?> getTypeAdapter(ConstructorConstructor constructorConstructor, Gson gson,
|
||||||
TypeToken<?> type, JsonAdapter annotation) {
|
TypeToken<?> type, JsonAdapter annotation, boolean isClassAnnotation) {
|
||||||
// TODO: The exception messages created by ConstructorConstructor are currently written in the context of
|
Object instance = createAdapter(constructorConstructor, annotation.value());
|
||||||
// deserialization and for example suggest usage of TypeAdapter, which would not work for @JsonAdapter usage
|
|
||||||
Object instance = constructorConstructor.get(TypeToken.get(annotation.value())).construct();
|
|
||||||
|
|
||||||
TypeAdapter<?> typeAdapter;
|
TypeAdapter<?> typeAdapter;
|
||||||
boolean nullSafe = annotation.nullSafe();
|
boolean nullSafe = annotation.nullSafe();
|
||||||
if (instance instanceof TypeAdapter) {
|
if (instance instanceof TypeAdapter) {
|
||||||
typeAdapter = (TypeAdapter<?>) instance;
|
typeAdapter = (TypeAdapter<?>) instance;
|
||||||
} else if (instance instanceof TypeAdapterFactory) {
|
} else if (instance instanceof TypeAdapterFactory) {
|
||||||
typeAdapter = ((TypeAdapterFactory) instance).create(gson, type);
|
TypeAdapterFactory factory = (TypeAdapterFactory) instance;
|
||||||
|
|
||||||
|
if (isClassAnnotation) {
|
||||||
|
factory = putFactoryAndGetCurrent(type.getRawType(), factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
typeAdapter = factory.create(gson, type);
|
||||||
} else if (instance instanceof JsonSerializer || instance instanceof JsonDeserializer) {
|
} else if (instance instanceof JsonSerializer || instance instanceof JsonDeserializer) {
|
||||||
JsonSerializer<?> serializer = instance instanceof JsonSerializer
|
JsonSerializer<?> serializer = instance instanceof JsonSerializer
|
||||||
? (JsonSerializer<?>) instance
|
? (JsonSerializer<?>) instance
|
||||||
|
@ -69,8 +122,16 @@ public final class JsonAdapterAnnotationTypeAdapterFactory implements TypeAdapte
|
||||||
? (JsonDeserializer<?>) instance
|
? (JsonDeserializer<?>) instance
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
// Uses dummy factory instances because TreeTypeAdapter needs a 'skipPast' factory for `Gson.getDelegateAdapter`
|
||||||
|
// call and has to differentiate there whether TreeTypeAdapter was created for @JsonAdapter on class or field
|
||||||
|
TypeAdapterFactory skipPast;
|
||||||
|
if (isClassAnnotation) {
|
||||||
|
skipPast = TREE_TYPE_CLASS_DUMMY_FACTORY;
|
||||||
|
} else {
|
||||||
|
skipPast = TREE_TYPE_FIELD_DUMMY_FACTORY;
|
||||||
|
}
|
||||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||||
TypeAdapter<?> tempAdapter = new TreeTypeAdapter(serializer, deserializer, gson, type, null, nullSafe);
|
TypeAdapter<?> tempAdapter = new TreeTypeAdapter(serializer, deserializer, gson, type, skipPast, nullSafe);
|
||||||
typeAdapter = tempAdapter;
|
typeAdapter = tempAdapter;
|
||||||
|
|
||||||
nullSafe = false;
|
nullSafe = false;
|
||||||
|
@ -87,4 +148,45 @@ public final class JsonAdapterAnnotationTypeAdapterFactory implements TypeAdapte
|
||||||
|
|
||||||
return typeAdapter;
|
return typeAdapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether {@code factory} is a type adapter factory created for {@code @JsonAdapter}
|
||||||
|
* placed on {@code type}.
|
||||||
|
*/
|
||||||
|
public boolean isClassJsonAdapterFactory(TypeToken<?> type, TypeAdapterFactory factory) {
|
||||||
|
Objects.requireNonNull(type);
|
||||||
|
Objects.requireNonNull(factory);
|
||||||
|
|
||||||
|
if (factory == TREE_TYPE_CLASS_DUMMY_FACTORY) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using raw type to match behavior of `create(Gson, TypeToken<T>)` above
|
||||||
|
Class<?> rawType = type.getRawType();
|
||||||
|
|
||||||
|
TypeAdapterFactory existingFactory = adapterFactoryMap.get(rawType);
|
||||||
|
if (existingFactory != null) {
|
||||||
|
// Checks for reference equality, like it is done by `Gson.getDelegateAdapter`
|
||||||
|
return existingFactory == factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no factory has been created for the type yet check manually for a @JsonAdapter annotation
|
||||||
|
// which specifies a TypeAdapterFactory
|
||||||
|
// Otherwise behavior would not be consistent, depending on whether or not adapter had been requested
|
||||||
|
// before call to `isClassJsonAdapterFactory` was made
|
||||||
|
JsonAdapter annotation = getAnnotation(rawType);
|
||||||
|
if (annotation == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Class<?> adapterClass = annotation.value();
|
||||||
|
if (!TypeAdapterFactory.class.isAssignableFrom(adapterClass)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object adapter = createAdapter(constructorConstructor, adapterClass);
|
||||||
|
TypeAdapterFactory newFactory = (TypeAdapterFactory) adapter;
|
||||||
|
|
||||||
|
return putFactoryAndGetCurrent(rawType, newFactory) == factory;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -156,7 +156,7 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
|
||||||
if (annotation != null) {
|
if (annotation != null) {
|
||||||
// This is not safe; requires that user has specified correct adapter class for @JsonAdapter
|
// This is not safe; requires that user has specified correct adapter class for @JsonAdapter
|
||||||
mapped = jsonAdapterFactory.getTypeAdapter(
|
mapped = jsonAdapterFactory.getTypeAdapter(
|
||||||
constructorConstructor, context, fieldType, annotation);
|
constructorConstructor, context, fieldType, annotation, false);
|
||||||
}
|
}
|
||||||
final boolean jsonAdapterPresent = mapped != null;
|
final boolean jsonAdapterPresent = mapped != null;
|
||||||
if (mapped == null) mapped = context.getAdapter(fieldType);
|
if (mapped == null) mapped = context.getAdapter(fieldType);
|
||||||
|
|
|
@ -43,11 +43,18 @@ public final class TreeTypeAdapter<T> extends SerializationDelegatingTypeAdapter
|
||||||
private final JsonDeserializer<T> deserializer;
|
private final JsonDeserializer<T> deserializer;
|
||||||
final Gson gson;
|
final Gson gson;
|
||||||
private final TypeToken<T> typeToken;
|
private final TypeToken<T> typeToken;
|
||||||
private final TypeAdapterFactory skipPast;
|
/**
|
||||||
|
* Only intended as {@code skipPast} for {@link Gson#getDelegateAdapter(TypeAdapterFactory, TypeToken)},
|
||||||
|
* must not be used in any other way.
|
||||||
|
*/
|
||||||
|
private final TypeAdapterFactory skipPastForGetDelegateAdapter;
|
||||||
private final GsonContextImpl context = new GsonContextImpl();
|
private final GsonContextImpl context = new GsonContextImpl();
|
||||||
private final boolean nullSafe;
|
private final boolean nullSafe;
|
||||||
|
|
||||||
/** The delegate is lazily created because it may not be needed, and creating it may fail. */
|
/**
|
||||||
|
* The delegate is lazily created because it may not be needed, and creating it may fail.
|
||||||
|
* Field has to be {@code volatile} because {@link Gson} guarantees to be thread-safe.
|
||||||
|
*/
|
||||||
private volatile TypeAdapter<T> delegate;
|
private volatile TypeAdapter<T> delegate;
|
||||||
|
|
||||||
public TreeTypeAdapter(JsonSerializer<T> serializer, JsonDeserializer<T> deserializer,
|
public TreeTypeAdapter(JsonSerializer<T> serializer, JsonDeserializer<T> deserializer,
|
||||||
|
@ -56,7 +63,7 @@ public final class TreeTypeAdapter<T> extends SerializationDelegatingTypeAdapter
|
||||||
this.deserializer = deserializer;
|
this.deserializer = deserializer;
|
||||||
this.gson = gson;
|
this.gson = gson;
|
||||||
this.typeToken = typeToken;
|
this.typeToken = typeToken;
|
||||||
this.skipPast = skipPast;
|
this.skipPastForGetDelegateAdapter = skipPast;
|
||||||
this.nullSafe = nullSafe;
|
this.nullSafe = nullSafe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +101,7 @@ public final class TreeTypeAdapter<T> extends SerializationDelegatingTypeAdapter
|
||||||
TypeAdapter<T> d = delegate;
|
TypeAdapter<T> d = delegate;
|
||||||
return d != null
|
return d != null
|
||||||
? d
|
? d
|
||||||
: (delegate = gson.getDelegateAdapter(skipPast, typeToken));
|
: (delegate = gson.getDelegateAdapter(skipPastForGetDelegateAdapter, typeToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -272,6 +272,90 @@ public final class GsonTest {
|
||||||
assertThat(otherThreadAdapter.get().toJson(null)).isEqualTo("[[\"wrapped-nested\"]]");
|
assertThat(otherThreadAdapter.get().toJson(null)).isEqualTo("[[\"wrapped-nested\"]]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetDelegateAdapter() {
|
||||||
|
class DummyAdapter extends TypeAdapter<Number> {
|
||||||
|
private final int number;
|
||||||
|
|
||||||
|
DummyAdapter(int number) {
|
||||||
|
this.number = number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Number read(JsonReader in) throws IOException {
|
||||||
|
throw new AssertionError("not needed for test");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(JsonWriter out, Number value) throws IOException {
|
||||||
|
throw new AssertionError("not needed for test");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override toString() for better assertion error messages
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "adapter-" + number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DummyFactory implements TypeAdapterFactory {
|
||||||
|
private final DummyAdapter adapter;
|
||||||
|
|
||||||
|
DummyFactory(DummyAdapter adapter) {
|
||||||
|
this.adapter = adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
|
||||||
|
return (TypeAdapter<T>) adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override equals to verify that reference equality check is performed by Gson,
|
||||||
|
// and this method is ignored
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
return obj instanceof DummyFactory && ((DummyFactory) obj).adapter.equals(adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return adapter.hashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DummyAdapter adapter1 = new DummyAdapter(1);
|
||||||
|
DummyFactory factory1 = new DummyFactory(adapter1);
|
||||||
|
DummyAdapter adapter2 = new DummyAdapter(2);
|
||||||
|
DummyFactory factory2 = new DummyFactory(adapter2);
|
||||||
|
|
||||||
|
Gson gson = new GsonBuilder()
|
||||||
|
// Note: This is 'last in, first out' order; Gson will first use factory2, then factory1
|
||||||
|
.registerTypeAdapterFactory(factory1)
|
||||||
|
.registerTypeAdapterFactory(factory2)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
TypeToken<?> type = TypeToken.get(Number.class);
|
||||||
|
|
||||||
|
assertThrows(NullPointerException.class, () -> gson.getDelegateAdapter(null, type));
|
||||||
|
assertThrows(NullPointerException.class, () -> gson.getDelegateAdapter(factory1, null));
|
||||||
|
|
||||||
|
// For unknown factory the first adapter for that type should be returned
|
||||||
|
assertThat(gson.getDelegateAdapter(new DummyFactory(new DummyAdapter(0)), type)).isEqualTo(adapter2);
|
||||||
|
|
||||||
|
assertThat(gson.getDelegateAdapter(factory2, type)).isEqualTo(adapter1);
|
||||||
|
// Default Gson adapter should be returned
|
||||||
|
assertThat(gson.getDelegateAdapter(factory1, type)).isNotInstanceOf(DummyAdapter.class);
|
||||||
|
|
||||||
|
DummyFactory factory1Eq = new DummyFactory(adapter1);
|
||||||
|
// Verify that test setup is correct
|
||||||
|
assertThat(factory1.equals(factory1Eq)).isTrue();
|
||||||
|
// Should only consider reference equality and ignore that custom `equals` method considers
|
||||||
|
// factories to be equal, therefore returning `adapter2` which came from `factory2` instead
|
||||||
|
// of skipping past `factory1`
|
||||||
|
assertThat(gson.getDelegateAdapter(factory1Eq, type)).isEqualTo(adapter2);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testNewJsonWriter_Default() throws IOException {
|
public void testNewJsonWriter_Default() throws IOException {
|
||||||
StringWriter writer = new StringWriter();
|
StringWriter writer = new StringWriter();
|
||||||
|
|
|
@ -33,7 +33,7 @@ import java.util.TreeSet;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Functional Test exercising custom serialization only. When test applies to both
|
* Functional Test exercising custom deserialization only. When test applies to both
|
||||||
* serialization and deserialization then add it to CustomTypeAdapterTest.
|
* serialization and deserialization then add it to CustomTypeAdapterTest.
|
||||||
*
|
*
|
||||||
* @author Inderjeet Singh
|
* @author Inderjeet Singh
|
||||||
|
|
|
@ -22,6 +22,7 @@ import static org.junit.Assert.fail;
|
||||||
import com.google.common.base.Splitter;
|
import com.google.common.base.Splitter;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.InstanceCreator;
|
||||||
import com.google.gson.JsonDeserializationContext;
|
import com.google.gson.JsonDeserializationContext;
|
||||||
import com.google.gson.JsonDeserializer;
|
import com.google.gson.JsonDeserializer;
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
|
@ -42,7 +43,7 @@ import java.util.Locale;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Functional tests for the {@link com.google.gson.annotations.JsonAdapter} annotation on classes.
|
* Functional tests for the {@link JsonAdapter} annotation on classes.
|
||||||
*/
|
*/
|
||||||
public final class JsonAdapterAnnotationOnClassesTest {
|
public final class JsonAdapterAnnotationOnClassesTest {
|
||||||
|
|
||||||
|
@ -274,4 +275,335 @@ public final class JsonAdapterAnnotationOnClassesTest {
|
||||||
private static final class D {
|
private static final class D {
|
||||||
@SuppressWarnings("unused") final String value = "a";
|
@SuppressWarnings("unused") final String value = "a";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that {@link TypeAdapterFactory} specified by {@code @JsonAdapter} can
|
||||||
|
* call {@link Gson#getDelegateAdapter} without any issues, despite the factory
|
||||||
|
* not being directly registered on Gson.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testDelegatingAdapterFactory() {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
WithDelegatingFactory<String> deserialized = new Gson().fromJson("{\"custom\":{\"f\":\"de\"}}", WithDelegatingFactory.class);
|
||||||
|
assertThat(deserialized.f).isEqualTo("de");
|
||||||
|
|
||||||
|
deserialized = new Gson().fromJson("{\"custom\":{\"f\":\"de\"}}", new TypeToken<WithDelegatingFactory<String>>() {});
|
||||||
|
assertThat(deserialized.f).isEqualTo("de");
|
||||||
|
|
||||||
|
WithDelegatingFactory<String> serialized = new WithDelegatingFactory<>("se");
|
||||||
|
assertThat(new Gson().toJson(serialized)).isEqualTo("{\"custom\":{\"f\":\"se\"}}");
|
||||||
|
}
|
||||||
|
@JsonAdapter(WithDelegatingFactory.Factory.class)
|
||||||
|
private static class WithDelegatingFactory<T> {
|
||||||
|
T f;
|
||||||
|
|
||||||
|
WithDelegatingFactory(T f) {
|
||||||
|
this.f = f;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Factory implements TypeAdapterFactory {
|
||||||
|
@Override
|
||||||
|
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
|
||||||
|
if (type.getRawType() != WithDelegatingFactory.class) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
|
||||||
|
|
||||||
|
return new TypeAdapter<T>() {
|
||||||
|
@Override
|
||||||
|
public T read(JsonReader in) throws IOException {
|
||||||
|
// Perform custom deserialization
|
||||||
|
in.beginObject();
|
||||||
|
assertThat(in.nextName()).isEqualTo("custom");
|
||||||
|
T t = delegate.read(in);
|
||||||
|
in.endObject();
|
||||||
|
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(JsonWriter out, T value) throws IOException {
|
||||||
|
// Perform custom serialization
|
||||||
|
out.beginObject();
|
||||||
|
out.name("custom");
|
||||||
|
delegate.write(out, value);
|
||||||
|
out.endObject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to {@link #testDelegatingAdapterFactory}, except that the delegate is not
|
||||||
|
* looked up in {@code create} but instead in the adapter methods.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testDelegatingAdapterFactory_Delayed() {
|
||||||
|
WithDelayedDelegatingFactory deserialized = new Gson().fromJson("{\"custom\":{\"f\":\"de\"}}", WithDelayedDelegatingFactory.class);
|
||||||
|
assertThat(deserialized.f).isEqualTo("de");
|
||||||
|
|
||||||
|
WithDelayedDelegatingFactory serialized = new WithDelayedDelegatingFactory("se");
|
||||||
|
assertThat(new Gson().toJson(serialized)).isEqualTo("{\"custom\":{\"f\":\"se\"}}");
|
||||||
|
}
|
||||||
|
@JsonAdapter(WithDelayedDelegatingFactory.Factory.class)
|
||||||
|
private static class WithDelayedDelegatingFactory {
|
||||||
|
String f;
|
||||||
|
|
||||||
|
WithDelayedDelegatingFactory(String f) {
|
||||||
|
this.f = f;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Factory implements TypeAdapterFactory {
|
||||||
|
@Override
|
||||||
|
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
|
||||||
|
return new TypeAdapter<T>() {
|
||||||
|
@SuppressWarnings("SameNameButDifferent") // suppress Error Prone warning; should be clear that `Factory` refers to enclosing class
|
||||||
|
private TypeAdapter<T> delegate() {
|
||||||
|
return gson.getDelegateAdapter(Factory.this, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T read(JsonReader in) throws IOException {
|
||||||
|
// Perform custom deserialization
|
||||||
|
in.beginObject();
|
||||||
|
assertThat(in.nextName()).isEqualTo("custom");
|
||||||
|
T t = delegate().read(in);
|
||||||
|
in.endObject();
|
||||||
|
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(JsonWriter out, T value) throws IOException {
|
||||||
|
// Perform custom serialization
|
||||||
|
out.beginObject();
|
||||||
|
out.name("custom");
|
||||||
|
delegate().write(out, value);
|
||||||
|
out.endObject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests behavior of {@link Gson#getDelegateAdapter} when <i>different</i> instances of the same
|
||||||
|
* factory class are used; one registered on the {@code GsonBuilder} and the other implicitly
|
||||||
|
* through {@code @JsonAdapter}.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testDelegating_SameFactoryClass() {
|
||||||
|
Gson gson = new GsonBuilder()
|
||||||
|
.registerTypeAdapterFactory(new WithDelegatingFactory.Factory())
|
||||||
|
.create();
|
||||||
|
|
||||||
|
// Should use both factories, and therefore have `{"custom": ... }` twice
|
||||||
|
WithDelegatingFactory<?> deserialized = gson.fromJson("{\"custom\":{\"custom\":{\"f\":\"de\"}}}", WithDelegatingFactory.class);
|
||||||
|
assertThat(deserialized.f).isEqualTo("de");
|
||||||
|
|
||||||
|
WithDelegatingFactory<String> serialized = new WithDelegatingFactory<>("se");
|
||||||
|
assertThat(gson.toJson(serialized)).isEqualTo("{\"custom\":{\"custom\":{\"f\":\"se\"}}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests behavior of {@link Gson#getDelegateAdapter} when the <i>same</i> instance of a factory
|
||||||
|
* is used (through {@link InstanceCreator}).
|
||||||
|
*
|
||||||
|
* <p><b>Important:</b> This situation is likely a rare corner case; the purpose of this test is
|
||||||
|
* to verify that Gson behaves reasonable, mainly that it does not cause a {@link StackOverflowError}
|
||||||
|
* due to infinite recursion. This test is not intended to dictate an expected behavior.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testDelegating_SameFactoryInstance() {
|
||||||
|
WithDelegatingFactory.Factory factory = new WithDelegatingFactory.Factory();
|
||||||
|
|
||||||
|
Gson gson = new GsonBuilder()
|
||||||
|
.registerTypeAdapterFactory(factory)
|
||||||
|
// Always provides same instance for factory
|
||||||
|
.registerTypeAdapter(WithDelegatingFactory.Factory.class, (InstanceCreator<?>) type -> factory)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
// Current Gson.getDelegateAdapter implementation cannot tell when call is related to @JsonAdapter
|
||||||
|
// or not, it can only work based on the `skipPast` factory, so if the same factory instance is used
|
||||||
|
// the one registered with `GsonBuilder.registerTypeAdapterFactory` actually skips past the @JsonAdapter
|
||||||
|
// one, so the JSON string is `{"custom": ...}` instead of `{"custom":{"custom":...}}`
|
||||||
|
WithDelegatingFactory<?> deserialized = gson.fromJson("{\"custom\":{\"f\":\"de\"}}", WithDelegatingFactory.class);
|
||||||
|
assertThat(deserialized.f).isEqualTo("de");
|
||||||
|
|
||||||
|
WithDelegatingFactory<String> serialized = new WithDelegatingFactory<>("se");
|
||||||
|
assertThat(gson.toJson(serialized)).isEqualTo("{\"custom\":{\"f\":\"se\"}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests behavior of {@link Gson#getDelegateAdapter} when <i>different</i> instances of the same
|
||||||
|
* factory class are used; one specified with {@code @JsonAdapter} on a class, and the other specified
|
||||||
|
* with {@code @JsonAdapter} on a field of that class.
|
||||||
|
*
|
||||||
|
* <p><b>Important:</b> This situation is likely a rare corner case; the purpose of this test is
|
||||||
|
* to verify that Gson behaves reasonable, mainly that it does not cause a {@link StackOverflowError}
|
||||||
|
* due to infinite recursion. This test is not intended to dictate an expected behavior.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testDelegating_SameFactoryClass_OnClassAndField() {
|
||||||
|
Gson gson = new GsonBuilder()
|
||||||
|
.registerTypeAdapter(String.class, new TypeAdapter<String>() {
|
||||||
|
@Override
|
||||||
|
public String read(JsonReader in) throws IOException {
|
||||||
|
return in.nextString() + "-str";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(JsonWriter out, String value) throws IOException {
|
||||||
|
out.value(value + "-str");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.create();
|
||||||
|
|
||||||
|
// Should use both factories, and therefore have `{"custom": ... }` once for class and once for the field,
|
||||||
|
// and for field also properly delegate to custom String adapter defined above
|
||||||
|
WithDelegatingFactoryOnClassAndField deserialized = gson.fromJson("{\"custom\":{\"f\":{\"custom\":\"de\"}}}",
|
||||||
|
WithDelegatingFactoryOnClassAndField.class);
|
||||||
|
assertThat(deserialized.f).isEqualTo("de-str");
|
||||||
|
|
||||||
|
WithDelegatingFactoryOnClassAndField serialized = new WithDelegatingFactoryOnClassAndField("se");
|
||||||
|
assertThat(gson.toJson(serialized)).isEqualTo("{\"custom\":{\"f\":{\"custom\":\"se-str\"}}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests behavior of {@link Gson#getDelegateAdapter} when the <i>same</i> instance of a factory
|
||||||
|
* is used (through {@link InstanceCreator}); specified with {@code @JsonAdapter} on a class,
|
||||||
|
* and also specified with {@code @JsonAdapter} on a field of that class.
|
||||||
|
*
|
||||||
|
* <p><b>Important:</b> This situation is likely a rare corner case; the purpose of this test is
|
||||||
|
* to verify that Gson behaves reasonable, mainly that it does not cause a {@link StackOverflowError}
|
||||||
|
* due to infinite recursion. This test is not intended to dictate an expected behavior.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testDelegating_SameFactoryInstance_OnClassAndField() {
|
||||||
|
WithDelegatingFactoryOnClassAndField.Factory factory = new WithDelegatingFactoryOnClassAndField.Factory();
|
||||||
|
|
||||||
|
Gson gson = new GsonBuilder()
|
||||||
|
.registerTypeAdapter(String.class, new TypeAdapter<String>() {
|
||||||
|
@Override
|
||||||
|
public String read(JsonReader in) throws IOException {
|
||||||
|
return in.nextString() + "-str";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(JsonWriter out, String value) throws IOException {
|
||||||
|
out.value(value + "-str");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Always provides same instance for factory
|
||||||
|
.registerTypeAdapter(WithDelegatingFactoryOnClassAndField.Factory.class, (InstanceCreator<?>) type -> factory)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
// Because field type (`String`) differs from declaring class, JsonAdapterAnnotationTypeAdapterFactory does
|
||||||
|
// not confuse factories and this behaves as expected: Both the declaring class and the field each have
|
||||||
|
// `{"custom": ...}` and delegation for the field to the custom String adapter defined above works properly
|
||||||
|
WithDelegatingFactoryOnClassAndField deserialized = gson.fromJson("{\"custom\":{\"f\":{\"custom\":\"de\"}}}",
|
||||||
|
WithDelegatingFactoryOnClassAndField.class);
|
||||||
|
assertThat(deserialized.f).isEqualTo("de-str");
|
||||||
|
|
||||||
|
WithDelegatingFactoryOnClassAndField serialized = new WithDelegatingFactoryOnClassAndField("se");
|
||||||
|
assertThat(gson.toJson(serialized)).isEqualTo("{\"custom\":{\"f\":{\"custom\":\"se-str\"}}}");
|
||||||
|
}
|
||||||
|
// Same factory class specified on class and one of its fields
|
||||||
|
@JsonAdapter(WithDelegatingFactoryOnClassAndField.Factory.class)
|
||||||
|
private static class WithDelegatingFactoryOnClassAndField {
|
||||||
|
@SuppressWarnings("SameNameButDifferent") // suppress Error Prone warning; should be clear that `Factory` refers to nested class
|
||||||
|
@JsonAdapter(Factory.class)
|
||||||
|
String f;
|
||||||
|
|
||||||
|
WithDelegatingFactoryOnClassAndField(String f) {
|
||||||
|
this.f = f;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Factory implements TypeAdapterFactory {
|
||||||
|
@Override
|
||||||
|
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
|
||||||
|
TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
|
||||||
|
|
||||||
|
return new TypeAdapter<T>() {
|
||||||
|
@Override
|
||||||
|
public T read(JsonReader in) throws IOException {
|
||||||
|
// Perform custom deserialization
|
||||||
|
in.beginObject();
|
||||||
|
assertThat(in.nextName()).isEqualTo("custom");
|
||||||
|
T t = delegate.read(in);
|
||||||
|
in.endObject();
|
||||||
|
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(JsonWriter out, T value) throws IOException {
|
||||||
|
// Perform custom serialization
|
||||||
|
out.beginObject();
|
||||||
|
out.name("custom");
|
||||||
|
delegate.write(out, value);
|
||||||
|
out.endObject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests usage of {@link JsonSerializer} as {@link JsonAdapter} value
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testJsonSerializer() {
|
||||||
|
Gson gson = new Gson();
|
||||||
|
// Verify that delegate deserializer (reflection deserializer) is used
|
||||||
|
WithJsonSerializer deserialized = gson.fromJson("{\"f\":\"test\"}", WithJsonSerializer.class);
|
||||||
|
assertThat(deserialized.f).isEqualTo("test");
|
||||||
|
|
||||||
|
String json = gson.toJson(new WithJsonSerializer());
|
||||||
|
// Uses custom serializer which always returns `true`
|
||||||
|
assertThat(json).isEqualTo("true");
|
||||||
|
}
|
||||||
|
@JsonAdapter(WithJsonSerializer.Serializer.class)
|
||||||
|
private static class WithJsonSerializer {
|
||||||
|
String f = "";
|
||||||
|
|
||||||
|
static class Serializer implements JsonSerializer<WithJsonSerializer> {
|
||||||
|
@Override
|
||||||
|
public JsonElement serialize(WithJsonSerializer src, Type typeOfSrc, JsonSerializationContext context) {
|
||||||
|
return new JsonPrimitive(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests usage of {@link JsonDeserializer} as {@link JsonAdapter} value
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testJsonDeserializer() {
|
||||||
|
Gson gson = new Gson();
|
||||||
|
WithJsonDeserializer deserialized = gson.fromJson("{\"f\":\"test\"}", WithJsonDeserializer.class);
|
||||||
|
// Uses custom deserializer which always uses "123" as field value
|
||||||
|
assertThat(deserialized.f).isEqualTo("123");
|
||||||
|
|
||||||
|
// Verify that delegate serializer (reflection serializer) is used
|
||||||
|
String json = gson.toJson(new WithJsonDeserializer("abc"));
|
||||||
|
assertThat(json).isEqualTo("{\"f\":\"abc\"}");
|
||||||
|
}
|
||||||
|
@JsonAdapter(WithJsonDeserializer.Deserializer.class)
|
||||||
|
private static class WithJsonDeserializer {
|
||||||
|
String f;
|
||||||
|
|
||||||
|
WithJsonDeserializer(String f) {
|
||||||
|
this.f = f;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Deserializer implements JsonDeserializer<WithJsonDeserializer> {
|
||||||
|
@Override
|
||||||
|
public WithJsonDeserializer deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
|
||||||
|
return new WithJsonDeserializer("123");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,21 +18,32 @@ package com.google.gson.functional;
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import com.google.gson.ExclusionStrategy;
|
||||||
|
import com.google.gson.FieldAttributes;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonDeserializationContext;
|
||||||
|
import com.google.gson.JsonDeserializer;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonPrimitive;
|
||||||
|
import com.google.gson.JsonSerializationContext;
|
||||||
|
import com.google.gson.JsonSerializer;
|
||||||
import com.google.gson.TypeAdapter;
|
import com.google.gson.TypeAdapter;
|
||||||
import com.google.gson.TypeAdapterFactory;
|
import com.google.gson.TypeAdapterFactory;
|
||||||
import com.google.gson.annotations.JsonAdapter;
|
import com.google.gson.annotations.JsonAdapter;
|
||||||
|
import com.google.gson.internal.bind.ReflectiveTypeAdapterFactory;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import com.google.gson.stream.JsonReader;
|
import com.google.gson.stream.JsonReader;
|
||||||
import com.google.gson.stream.JsonWriter;
|
import com.google.gson.stream.JsonWriter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Functional tests for the {@link com.google.gson.annotations.JsonAdapter} annotation on fields.
|
* Functional tests for the {@link JsonAdapter} annotation on fields.
|
||||||
*/
|
*/
|
||||||
public final class JsonAdapterAnnotationOnFieldsTest {
|
public final class JsonAdapterAnnotationOnFieldsTest {
|
||||||
@Test
|
@Test
|
||||||
|
@ -313,4 +324,339 @@ public final class JsonAdapterAnnotationOnFieldsTest {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that {@link JsonAdapter} annotation can overwrite adapters which
|
||||||
|
* can normally not be overwritten (in this case adapter for {@link JsonElement}).
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testOverwriteBuiltIn() {
|
||||||
|
BuiltInOverwriting obj = new BuiltInOverwriting();
|
||||||
|
obj.f = new JsonPrimitive(true);
|
||||||
|
String json = new Gson().toJson(obj);
|
||||||
|
assertThat(json).isEqualTo("{\"f\":\"" + JsonElementAdapter.SERIALIZED + "\"}");
|
||||||
|
|
||||||
|
BuiltInOverwriting deserialized = new Gson().fromJson("{\"f\": 2}", BuiltInOverwriting.class);
|
||||||
|
assertThat(deserialized.f).isEqualTo(JsonElementAdapter.DESERIALIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class BuiltInOverwriting {
|
||||||
|
@JsonAdapter(JsonElementAdapter.class)
|
||||||
|
JsonElement f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class JsonElementAdapter extends TypeAdapter<JsonElement> {
|
||||||
|
static final JsonPrimitive DESERIALIZED = new JsonPrimitive("deserialized hardcoded");
|
||||||
|
@Override public JsonElement read(JsonReader in) throws IOException {
|
||||||
|
in.skipValue();
|
||||||
|
return DESERIALIZED;
|
||||||
|
}
|
||||||
|
|
||||||
|
static final String SERIALIZED = "serialized hardcoded";
|
||||||
|
@Override public void write(JsonWriter out, JsonElement value) throws IOException {
|
||||||
|
out.value(SERIALIZED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that exclusion strategy preventing serialization has higher precedence than
|
||||||
|
* {@link JsonAdapter} annotation.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testExcludeSerializePrecedence() {
|
||||||
|
Gson gson = new GsonBuilder()
|
||||||
|
.addSerializationExclusionStrategy(new ExclusionStrategy() {
|
||||||
|
@Override public boolean shouldSkipField(FieldAttributes f) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
@Override public boolean shouldSkipClass(Class<?> clazz) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.create();
|
||||||
|
|
||||||
|
DelegatingAndOverwriting obj = new DelegatingAndOverwriting();
|
||||||
|
obj.f = 1;
|
||||||
|
obj.f2 = new JsonPrimitive(2);
|
||||||
|
obj.f3 = new JsonPrimitive(true);
|
||||||
|
String json = gson.toJson(obj);
|
||||||
|
assertThat(json).isEqualTo("{}");
|
||||||
|
|
||||||
|
DelegatingAndOverwriting deserialized = gson.fromJson("{\"f\":1,\"f2\":2,\"f3\":3}", DelegatingAndOverwriting.class);
|
||||||
|
assertThat(deserialized.f).isEqualTo(Integer.valueOf(1));
|
||||||
|
assertThat(deserialized.f2).isEqualTo(new JsonPrimitive(2));
|
||||||
|
// Verify that for deserialization type adapter specified by @JsonAdapter is used
|
||||||
|
assertThat(deserialized.f3).isEqualTo(JsonElementAdapter.DESERIALIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that exclusion strategy preventing deserialization has higher precedence than
|
||||||
|
* {@link JsonAdapter} annotation.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testExcludeDeserializePrecedence() {
|
||||||
|
Gson gson = new GsonBuilder()
|
||||||
|
.addDeserializationExclusionStrategy(new ExclusionStrategy() {
|
||||||
|
@Override public boolean shouldSkipField(FieldAttributes f) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
@Override public boolean shouldSkipClass(Class<?> clazz) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.create();
|
||||||
|
|
||||||
|
DelegatingAndOverwriting obj = new DelegatingAndOverwriting();
|
||||||
|
obj.f = 1;
|
||||||
|
obj.f2 = new JsonPrimitive(2);
|
||||||
|
obj.f3 = new JsonPrimitive(true);
|
||||||
|
String json = gson.toJson(obj);
|
||||||
|
// Verify that for serialization type adapters specified by @JsonAdapter are used
|
||||||
|
assertThat(json).isEqualTo("{\"f\":1,\"f2\":2,\"f3\":\"" + JsonElementAdapter.SERIALIZED + "\"}");
|
||||||
|
|
||||||
|
DelegatingAndOverwriting deserialized = gson.fromJson("{\"f\":1,\"f2\":2,\"f3\":3}", DelegatingAndOverwriting.class);
|
||||||
|
assertThat(deserialized.f).isNull();
|
||||||
|
assertThat(deserialized.f2).isNull();
|
||||||
|
assertThat(deserialized.f3).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that exclusion strategy preventing serialization and deserialization has
|
||||||
|
* higher precedence than {@link JsonAdapter} annotation.
|
||||||
|
*
|
||||||
|
* <p>This is a separate test method because {@link ReflectiveTypeAdapterFactory} handles
|
||||||
|
* this case differently.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testExcludePrecedence() {
|
||||||
|
Gson gson = new GsonBuilder()
|
||||||
|
.setExclusionStrategies(new ExclusionStrategy() {
|
||||||
|
@Override public boolean shouldSkipField(FieldAttributes f) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
@Override public boolean shouldSkipClass(Class<?> clazz) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.create();
|
||||||
|
|
||||||
|
DelegatingAndOverwriting obj = new DelegatingAndOverwriting();
|
||||||
|
obj.f = 1;
|
||||||
|
obj.f2 = new JsonPrimitive(2);
|
||||||
|
obj.f3 = new JsonPrimitive(true);
|
||||||
|
String json = gson.toJson(obj);
|
||||||
|
assertThat(json).isEqualTo("{}");
|
||||||
|
|
||||||
|
DelegatingAndOverwriting deserialized = gson.fromJson("{\"f\":1,\"f2\":2,\"f3\":3}", DelegatingAndOverwriting.class);
|
||||||
|
assertThat(deserialized.f).isNull();
|
||||||
|
assertThat(deserialized.f2).isNull();
|
||||||
|
assertThat(deserialized.f3).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DelegatingAndOverwriting {
|
||||||
|
@JsonAdapter(DelegatingAdapterFactory.class)
|
||||||
|
Integer f;
|
||||||
|
@JsonAdapter(DelegatingAdapterFactory.class)
|
||||||
|
JsonElement f2;
|
||||||
|
// Also have non-delegating adapter to make tests handle both cases
|
||||||
|
@JsonAdapter(JsonElementAdapter.class)
|
||||||
|
JsonElement f3;
|
||||||
|
|
||||||
|
static class DelegatingAdapterFactory implements TypeAdapterFactory {
|
||||||
|
@Override
|
||||||
|
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
|
||||||
|
return gson.getDelegateAdapter(this, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that {@link TypeAdapterFactory} specified by {@code @JsonAdapter} can
|
||||||
|
* call {@link Gson#getDelegateAdapter} without any issues, despite the factory
|
||||||
|
* not being directly registered on Gson.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testDelegatingAdapterFactory() {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
WithDelegatingFactory<String> deserialized = new Gson().fromJson("{\"f\":\"test\"}", WithDelegatingFactory.class);
|
||||||
|
assertThat(deserialized.f).isEqualTo("test-custom");
|
||||||
|
|
||||||
|
deserialized = new Gson().fromJson("{\"f\":\"test\"}", new TypeToken<WithDelegatingFactory<String>>() {});
|
||||||
|
assertThat(deserialized.f).isEqualTo("test-custom");
|
||||||
|
|
||||||
|
WithDelegatingFactory<String> serialized = new WithDelegatingFactory<>();
|
||||||
|
serialized.f = "value";
|
||||||
|
assertThat(new Gson().toJson(serialized)).isEqualTo("{\"f\":\"value-custom\"}");
|
||||||
|
}
|
||||||
|
private static class WithDelegatingFactory<T> {
|
||||||
|
@SuppressWarnings("SameNameButDifferent") // suppress Error Prone warning; should be clear that `Factory` refers to nested class
|
||||||
|
@JsonAdapter(Factory.class)
|
||||||
|
T f;
|
||||||
|
|
||||||
|
static class Factory implements TypeAdapterFactory {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
|
||||||
|
TypeAdapter<String> delegate = (TypeAdapter<String>) gson.getDelegateAdapter(this, type);
|
||||||
|
|
||||||
|
return (TypeAdapter<T>) new TypeAdapter<String>() {
|
||||||
|
@Override
|
||||||
|
public String read(JsonReader in) throws IOException {
|
||||||
|
// Perform custom deserialization
|
||||||
|
return delegate.read(in) + "-custom";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(JsonWriter out, String value) throws IOException {
|
||||||
|
// Perform custom serialization
|
||||||
|
delegate.write(out, value + "-custom");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to {@link #testDelegatingAdapterFactory}, except that the delegate is not
|
||||||
|
* looked up in {@code create} but instead in the adapter methods.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testDelegatingAdapterFactory_Delayed() {
|
||||||
|
WithDelayedDelegatingFactory deserialized = new Gson().fromJson("{\"f\":\"test\"}", WithDelayedDelegatingFactory.class);
|
||||||
|
assertThat(deserialized.f).isEqualTo("test-custom");
|
||||||
|
|
||||||
|
WithDelayedDelegatingFactory serialized = new WithDelayedDelegatingFactory();
|
||||||
|
serialized.f = "value";
|
||||||
|
assertThat(new Gson().toJson(serialized)).isEqualTo("{\"f\":\"value-custom\"}");
|
||||||
|
}
|
||||||
|
@SuppressWarnings("SameNameButDifferent") // suppress Error Prone warning; should be clear that `Factory` refers to nested class
|
||||||
|
private static class WithDelayedDelegatingFactory {
|
||||||
|
@JsonAdapter(Factory.class)
|
||||||
|
String f;
|
||||||
|
|
||||||
|
static class Factory implements TypeAdapterFactory {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
|
||||||
|
return (TypeAdapter<T>) new TypeAdapter<String>() {
|
||||||
|
private TypeAdapter<String> delegate() {
|
||||||
|
return (TypeAdapter<String>) gson.getDelegateAdapter(Factory.this, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String read(JsonReader in) throws IOException {
|
||||||
|
// Perform custom deserialization
|
||||||
|
return delegate().read(in) + "-custom";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(JsonWriter out, String value) throws IOException {
|
||||||
|
// Perform custom serialization
|
||||||
|
delegate().write(out, value + "-custom");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests usage of {@link Gson#getAdapter(TypeToken)} in the {@code create} method of the factory.
|
||||||
|
* Existing code was using that as workaround because {@link Gson#getDelegateAdapter} previously
|
||||||
|
* did not work in combination with {@code @JsonAdapter}, see https://github.com/google/gson/issues/1028.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testGetAdapterDelegation() {
|
||||||
|
Gson gson = new Gson();
|
||||||
|
GetAdapterDelegation deserialized = gson.fromJson("{\"f\":\"de\"}", GetAdapterDelegation.class);
|
||||||
|
assertThat(deserialized.f).isEqualTo("de-custom");
|
||||||
|
|
||||||
|
String json = gson.toJson(new GetAdapterDelegation("se"));
|
||||||
|
assertThat(json).isEqualTo("{\"f\":\"se-custom\"}");
|
||||||
|
}
|
||||||
|
private static class GetAdapterDelegation {
|
||||||
|
@SuppressWarnings("SameNameButDifferent") // suppress Error Prone warning; should be clear that `Factory` refers to nested class
|
||||||
|
@JsonAdapter(Factory.class)
|
||||||
|
String f;
|
||||||
|
|
||||||
|
GetAdapterDelegation(String f) {
|
||||||
|
this.f = f;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Factory implements TypeAdapterFactory {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
|
||||||
|
// Uses `Gson.getAdapter` instead of `Gson.getDelegateAdapter`
|
||||||
|
TypeAdapter<String> delegate = (TypeAdapter<String>) gson.getAdapter(type);
|
||||||
|
|
||||||
|
return (TypeAdapter<T>) new TypeAdapter<String>() {
|
||||||
|
@Override
|
||||||
|
public String read(JsonReader in) throws IOException {
|
||||||
|
return delegate.read(in) + "-custom";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(JsonWriter out, String value) throws IOException {
|
||||||
|
delegate.write(out, value + "-custom");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests usage of {@link JsonSerializer} as {@link JsonAdapter} value on a field
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testJsonSerializer() {
|
||||||
|
Gson gson = new Gson();
|
||||||
|
// Verify that delegate deserializer for List is used
|
||||||
|
WithJsonSerializer deserialized = gson.fromJson("{\"f\":[1,2,3]}", WithJsonSerializer.class);
|
||||||
|
assertThat(deserialized.f).isEqualTo(Arrays.asList(1, 2, 3));
|
||||||
|
|
||||||
|
String json = gson.toJson(new WithJsonSerializer());
|
||||||
|
// Uses custom serializer which always returns `true`
|
||||||
|
assertThat(json).isEqualTo("{\"f\":true}");
|
||||||
|
}
|
||||||
|
private static class WithJsonSerializer {
|
||||||
|
@JsonAdapter(Serializer.class)
|
||||||
|
List<Integer> f = Collections.emptyList();
|
||||||
|
|
||||||
|
static class Serializer implements JsonSerializer<List<Integer>> {
|
||||||
|
@Override
|
||||||
|
public JsonElement serialize(List<Integer> src, Type typeOfSrc, JsonSerializationContext context) {
|
||||||
|
return new JsonPrimitive(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests usage of {@link JsonDeserializer} as {@link JsonAdapter} value on a field
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testJsonDeserializer() {
|
||||||
|
Gson gson = new Gson();
|
||||||
|
WithJsonDeserializer deserialized = gson.fromJson("{\"f\":[5]}", WithJsonDeserializer.class);
|
||||||
|
// Uses custom deserializer which always returns `[3, 2, 1]`
|
||||||
|
assertThat(deserialized.f).isEqualTo(Arrays.asList(3, 2, 1));
|
||||||
|
|
||||||
|
// Verify that delegate serializer for List is used
|
||||||
|
String json = gson.toJson(new WithJsonDeserializer(Arrays.asList(4, 5, 6)));
|
||||||
|
assertThat(json).isEqualTo("{\"f\":[4,5,6]}");
|
||||||
|
}
|
||||||
|
private static class WithJsonDeserializer {
|
||||||
|
@JsonAdapter(Deserializer.class)
|
||||||
|
List<Integer> f;
|
||||||
|
|
||||||
|
WithJsonDeserializer(List<Integer> f) {
|
||||||
|
this.f = f;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Deserializer implements JsonDeserializer<List<Integer>> {
|
||||||
|
@Override
|
||||||
|
public List<Integer> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
|
||||||
|
return Arrays.asList(3, 2, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue