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;
|
||||
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import com.google.gson.internal.ConstructorConstructor;
|
||||
import com.google.gson.internal.Excluder;
|
||||
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
|
||||
* 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
|
||||
* from or written to JSON. We can achieve this by writing a type adapter factory that uses
|
||||
* the <code>getDelegateAdapter</code> method:
|
||||
* <pre> {@code
|
||||
* class StatsTypeAdapterFactory implements TypeAdapterFactory {
|
||||
* public int numReads = 0;
|
||||
* public int numWrites = 0;
|
||||
* public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
|
||||
* final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
|
||||
* return new TypeAdapter<T>() {
|
||||
* public void write(JsonWriter out, T value) throws IOException {
|
||||
* ++numWrites;
|
||||
* delegate.write(out, value);
|
||||
* }
|
||||
* public T read(JsonReader in) throws IOException {
|
||||
* ++numReads;
|
||||
* return delegate.read(in);
|
||||
* }
|
||||
* };
|
||||
* }
|
||||
* }
|
||||
* } </pre>
|
||||
* This factory can now be used like this:
|
||||
* <pre> {@code
|
||||
* StatsTypeAdapterFactory stats = new StatsTypeAdapterFactory();
|
||||
* Gson gson = new GsonBuilder().registerTypeAdapterFactory(stats).create();
|
||||
* // Call gson.toJson() and fromJson methods on objects
|
||||
* System.out.println("Num JSON reads" + stats.numReads);
|
||||
* System.out.println("Num JSON writes" + stats.numWrites);
|
||||
* }</pre>
|
||||
* 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
|
||||
* that the order of registration does not prevent this method from reaching a factory they
|
||||
* would expect to reply from this call.
|
||||
* Note that since you can not override type adapter factories for String and Java primitive
|
||||
* types, our stats factory will not count the number of String or primitives that will be
|
||||
* read or written.
|
||||
* from or written to JSON. We can achieve this by writing a type adapter factory that uses
|
||||
* the <code>getDelegateAdapter</code> method:
|
||||
* <pre>{@code
|
||||
* class StatsTypeAdapterFactory implements TypeAdapterFactory {
|
||||
* public int numReads = 0;
|
||||
* public int numWrites = 0;
|
||||
* public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
|
||||
* final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
|
||||
* return new TypeAdapter<T>() {
|
||||
* public void write(JsonWriter out, T value) throws IOException {
|
||||
* ++numWrites;
|
||||
* delegate.write(out, value);
|
||||
* }
|
||||
* public T read(JsonReader in) throws IOException {
|
||||
* ++numReads;
|
||||
* return delegate.read(in);
|
||||
* }
|
||||
* };
|
||||
* }
|
||||
* }
|
||||
* }</pre>
|
||||
* This factory can now be used like this:
|
||||
* <pre>{@code
|
||||
* StatsTypeAdapterFactory stats = new StatsTypeAdapterFactory();
|
||||
* Gson gson = new GsonBuilder().registerTypeAdapterFactory(stats).create();
|
||||
* // Call gson.toJson() and fromJson methods on objects
|
||||
* System.out.println("Num JSON reads: " + stats.numReads);
|
||||
* System.out.println("Num JSON writes: " + stats.numWrites);
|
||||
* }</pre>
|
||||
* 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
|
||||
* that the order of registration does not prevent this method from reaching a factory they
|
||||
* would expect to reply from this call.
|
||||
* Note that since you can not override the type adapter factories for some types, see
|
||||
* {@link GsonBuilder#registerTypeAdapter(Type, Object)}, our stats factory will not count
|
||||
* 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
|
||||
* 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).
|
||||
|
@ -648,9 +657,10 @@ public final class Gson {
|
|||
* @since 2.2
|
||||
*/
|
||||
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
|
||||
// our @JsonAdapter annotation.
|
||||
if (!factories.contains(skipPast)) {
|
||||
Objects.requireNonNull(skipPast, "skipPast must not be null");
|
||||
Objects.requireNonNull(type, "type must not be null");
|
||||
|
||||
if (jsonAdapterFactory.isClassJsonAdapterFactory(type, skipPast)) {
|
||||
skipPast = jsonAdapterFactory;
|
||||
}
|
||||
|
||||
|
@ -668,7 +678,13 @@ public final class Gson {
|
|||
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>
|
||||
*
|
||||
* <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.
|
||||
* 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
|
||||
* 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,
|
||||
* 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.internal.ConstructorConstructor;
|
||||
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
|
||||
|
@ -32,35 +35,85 @@ import com.google.gson.reflect.TypeToken;
|
|||
* @since 2.3
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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
|
||||
@Override
|
||||
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> targetType) {
|
||||
Class<? super T> rawType = targetType.getRawType();
|
||||
JsonAdapter annotation = rawType.getAnnotation(JsonAdapter.class);
|
||||
JsonAdapter annotation = getAnnotation(rawType);
|
||||
if (annotation == 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,
|
||||
TypeToken<?> type, JsonAdapter annotation) {
|
||||
// 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
|
||||
Object instance = constructorConstructor.get(TypeToken.get(annotation.value())).construct();
|
||||
TypeToken<?> type, JsonAdapter annotation, boolean isClassAnnotation) {
|
||||
Object instance = createAdapter(constructorConstructor, annotation.value());
|
||||
|
||||
TypeAdapter<?> typeAdapter;
|
||||
boolean nullSafe = annotation.nullSafe();
|
||||
if (instance instanceof TypeAdapter) {
|
||||
typeAdapter = (TypeAdapter<?>) instance;
|
||||
} 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) {
|
||||
JsonSerializer<?> serializer = instance instanceof JsonSerializer
|
||||
? (JsonSerializer<?>) instance
|
||||
|
@ -69,8 +122,16 @@ public final class JsonAdapterAnnotationTypeAdapterFactory implements TypeAdapte
|
|||
? (JsonDeserializer<?>) instance
|
||||
: 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" })
|
||||
TypeAdapter<?> tempAdapter = new TreeTypeAdapter(serializer, deserializer, gson, type, null, nullSafe);
|
||||
TypeAdapter<?> tempAdapter = new TreeTypeAdapter(serializer, deserializer, gson, type, skipPast, nullSafe);
|
||||
typeAdapter = tempAdapter;
|
||||
|
||||
nullSafe = false;
|
||||
|
@ -87,4 +148,45 @@ public final class JsonAdapterAnnotationTypeAdapterFactory implements TypeAdapte
|
|||
|
||||
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) {
|
||||
// This is not safe; requires that user has specified correct adapter class for @JsonAdapter
|
||||
mapped = jsonAdapterFactory.getTypeAdapter(
|
||||
constructorConstructor, context, fieldType, annotation);
|
||||
constructorConstructor, context, fieldType, annotation, false);
|
||||
}
|
||||
final boolean jsonAdapterPresent = mapped != null;
|
||||
if (mapped == null) mapped = context.getAdapter(fieldType);
|
||||
|
|
|
@ -43,11 +43,18 @@ public final class TreeTypeAdapter<T> extends SerializationDelegatingTypeAdapter
|
|||
private final JsonDeserializer<T> deserializer;
|
||||
final Gson gson;
|
||||
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 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;
|
||||
|
||||
public TreeTypeAdapter(JsonSerializer<T> serializer, JsonDeserializer<T> deserializer,
|
||||
|
@ -56,7 +63,7 @@ public final class TreeTypeAdapter<T> extends SerializationDelegatingTypeAdapter
|
|||
this.deserializer = deserializer;
|
||||
this.gson = gson;
|
||||
this.typeToken = typeToken;
|
||||
this.skipPast = skipPast;
|
||||
this.skipPastForGetDelegateAdapter = skipPast;
|
||||
this.nullSafe = nullSafe;
|
||||
}
|
||||
|
||||
|
@ -94,7 +101,7 @@ public final class TreeTypeAdapter<T> extends SerializationDelegatingTypeAdapter
|
|||
TypeAdapter<T> d = delegate;
|
||||
return d != null
|
||||
? 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\"]]");
|
||||
}
|
||||
|
||||
@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
|
||||
public void testNewJsonWriter_Default() throws IOException {
|
||||
StringWriter writer = new StringWriter();
|
||||
|
|
|
@ -33,7 +33,7 @@ import java.util.TreeSet;
|
|||
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.
|
||||
*
|
||||
* @author Inderjeet Singh
|
||||
|
|
|
@ -22,6 +22,7 @@ import static org.junit.Assert.fail;
|
|||
import com.google.common.base.Splitter;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.InstanceCreator;
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
|
@ -42,7 +43,7 @@ import java.util.Locale;
|
|||
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 {
|
||||
|
||||
|
@ -274,4 +275,335 @@ public final class JsonAdapterAnnotationOnClassesTest {
|
|||
private static final class D {
|
||||
@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 com.google.gson.ExclusionStrategy;
|
||||
import com.google.gson.FieldAttributes;
|
||||
import com.google.gson.Gson;
|
||||
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.TypeAdapterFactory;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import com.google.gson.internal.bind.ReflectiveTypeAdapterFactory;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
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 {
|
||||
@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
Block a user