From 9e77d391d60875bdfcba72a6b0181ba84d179f62 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sat, 15 Jul 2023 15:46:32 +0200 Subject: [PATCH] Fix `Gson.getDelegateAdapter` not working properly for `JsonAdapter` --- gson/src/main/java/com/google/gson/Gson.java | 100 ++--- .../java/com/google/gson/InstanceCreator.java | 4 +- .../gson/internal/ConstructorConstructor.java | 17 +- ...onAdapterAnnotationTypeAdapterFactory.java | 99 ++++- .../bind/ReflectiveTypeAdapterFactory.java | 2 +- .../gson/internal/bind/TreeTypeAdapter.java | 4 + .../test/java/com/google/gson/GsonTest.java | 64 ++++ .../gson/functional/InstanceCreatorTest.java | 47 ++- .../JsonAdapterAnnotationOnClassesTest.java | 330 ++++++++++++++++- .../JsonAdapterAnnotationOnFieldsTest.java | 344 +++++++++++++++++- 10 files changed, 955 insertions(+), 56 deletions(-) diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index c6f8508ef1..167778cbe0 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -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; @@ -593,54 +594,71 @@ public TypeAdapter getAdapter(TypeToken type) { * adapter that does a little bit of work but then delegates further processing to the Gson * default type adapter. Here is an example: *

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 getDelegateAdapter method: - *

 {@code
-   *  class StatsTypeAdapterFactory implements TypeAdapterFactory {
-   *    public int numReads = 0;
-   *    public int numWrites = 0;
-   *    public  TypeAdapter create(Gson gson, TypeToken type) {
-   *      final TypeAdapter delegate = gson.getDelegateAdapter(this, type);
-   *      return new TypeAdapter() {
-   *        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);
-   *        }
-   *      };
-   *    }
-   *  }
-   *  } 
- * This factory can now be used like this: - *
 {@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);
-   *  }
- * 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 getDelegateAdapter method: + *
{@code
+   * class StatsTypeAdapterFactory implements TypeAdapterFactory {
+   *   public int numReads = 0;
+   *   public int numWrites = 0;
+   *   public  TypeAdapter create(Gson gson, TypeToken type) {
+   *     final TypeAdapter delegate = gson.getDelegateAdapter(this, type);
+   *     return new TypeAdapter() {
+   *       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);
+   *       }
+   *     };
+   *   }
+   * }
+   * }
+ * This factory can now be used like this: + *
{@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);
+   * }
+ * 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. + * + *

If {@code skipPast} is {@code null} or 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 this (the type adapter - * factory from where {@code getDelegateAdapter} method is being invoked). + * factory from where {@code getDelegateAdapter} method is being invoked). May be {@code null}. * @param type Type for which the delegate adapter is being searched for. * * @since 2.2 */ public TypeAdapter getDelegateAdapter(TypeAdapterFactory skipPast, TypeToken type) { - // Hack. If the skipPast factory isn't registered, assume the factory is being requested via - // our @JsonAdapter annotation. - if (!factories.contains(skipPast)) { - skipPast = jsonAdapterFactory; + Objects.requireNonNull(type, "type must not be null"); + + if (skipPast != null) { + if (jsonAdapterFactory.isClassJsonAdapterFactory(type.getRawType(), skipPast)) { + skipPast = jsonAdapterFactory; + } else if (!factories.contains(skipPast)) { + // Probably a factory from @JsonAdapter on a field + skipPast = null; + } + } + + if (skipPast == null) { + return getAdapter(type); } boolean skipPastFound = false; diff --git a/gson/src/main/java/com/google/gson/InstanceCreator.java b/gson/src/main/java/com/google/gson/InstanceCreator.java index d5096a07a3..b973da07eb 100644 --- a/gson/src/main/java/com/google/gson/InstanceCreator.java +++ b/gson/src/main/java/com/google/gson/InstanceCreator.java @@ -63,7 +63,7 @@ * * *

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 * new 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:

* @@ -81,7 +81,7 @@ public interface InstanceCreator { /** * 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. * diff --git a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java index 7d2dc9b622..ce1e160746 100644 --- a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java +++ b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java @@ -88,6 +88,19 @@ static String checkInstantiable(Class c) { return null; } + private static T useInstanceCreator(InstanceCreator instanceCreator, Type type, Class rawType) { + T instance = instanceCreator.createInstance(type); + if (instance == null) { + throw new RuntimeException("InstanceCreator " + instanceCreator + " returned null for type " + type); + } + + if (!rawType.isInstance(instance)) { + throw new ClassCastException("InstanceCreator " + instanceCreator + " created instance of wrong type;" + + " expected " + rawType.getName() + " but got instance of unrelated type " + instance.getClass().getName()); + } + return instance; + } + public ObjectConstructor get(TypeToken typeToken) { final Type type = typeToken.getType(); final Class rawType = typeToken.getRawType(); @@ -99,7 +112,7 @@ public ObjectConstructor get(TypeToken typeToken) { if (typeCreator != null) { return new ObjectConstructor() { @Override public T construct() { - return typeCreator.createInstance(type); + return useInstanceCreator(typeCreator, type, rawType); } }; } @@ -111,7 +124,7 @@ public ObjectConstructor get(TypeToken typeToken) { if (rawTypeCreator != null) { return new ObjectConstructor() { @Override public T construct() { - return rawTypeCreator.createInstance(type); + return useInstanceCreator(rawTypeCreator, type, rawType); } }; } diff --git a/gson/src/main/java/com/google/gson/internal/bind/JsonAdapterAnnotationTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/JsonAdapterAnnotationTypeAdapterFactory.java index 9cd5649e9f..0a60ea1618 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/JsonAdapterAnnotationTypeAdapterFactory.java +++ b/gson/src/main/java/com/google/gson/internal/bind/JsonAdapterAnnotationTypeAdapterFactory.java @@ -24,6 +24,9 @@ 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,76 @@ * @since 2.3 */ public final class JsonAdapterAnnotationTypeAdapterFactory implements TypeAdapterFactory { + private static class DummyTypeAdapterFactory implements TypeAdapterFactory { + @Override public TypeAdapter create(Gson gson, TypeToken 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_DUMMY_FACTORY = new DummyTypeAdapterFactory(); + private final ConstructorConstructor constructorConstructor; + /** + * For a class, if it is annotated with {@code @JsonAdapter} and refers to a {@code TypeAdapterFactory} + * stores the factory instance, in case it has been requested already. + */ + // Note: In case these strong reference to TypeAdapterFactory instances are considered + // a memory leak in the future, could consider switching to WeakReference + private final ConcurrentMap, 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 TypeAdapter create(Gson gson, TypeToken targetType) { Class rawType = targetType.getRawType(); - JsonAdapter annotation = rawType.getAnnotation(JsonAdapter.class); + JsonAdapter annotation = getAnnotation(rawType); if (annotation == null) { return null; } - return (TypeAdapter) getTypeAdapter(constructorConstructor, gson, targetType, annotation); + return (TypeAdapter) getTypeAdapter(constructorConstructor, gson, targetType, annotation, true); } - TypeAdapter getTypeAdapter(ConstructorConstructor constructorConstructor, Gson gson, - TypeToken type, JsonAdapter annotation) { + // 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 - Object instance = constructorConstructor.get(TypeToken.get(annotation.value())).construct(); + 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, 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 +113,15 @@ TypeAdapter getTypeAdapter(ConstructorConstructor constructorConstructor, Gso ? (JsonDeserializer) instance : null; + TypeAdapterFactory skipPast = null; + if (isClassAnnotation) { + // Use dummy `skipPast` value; otherwise TreeTypeAdapter's call to `Gson.getDelegateAdapter` would + // cause infinite recursion because it would keep returning adapter specified by @JsonAdapter + skipPast = TREE_TYPE_DUMMY_FACTORY; + adapterFactoryMap.put(type.getRawType(), skipPast); + } @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 +138,38 @@ TypeAdapter getTypeAdapter(ConstructorConstructor constructorConstructor, Gso return typeAdapter; } + + /** + * Returns whether {@code factory} is a type adapter factory created for {@code @JsonAdapter} + * placed on {@code rawType}. + */ + public boolean isClassJsonAdapterFactory(Class rawType, TypeAdapterFactory factory) { + Objects.requireNonNull(rawType); + Objects.requireNonNull(factory); + + 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; + } } diff --git a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java index 6a43b817c9..d981f2c5fd 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java +++ b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java @@ -156,7 +156,7 @@ private BoundField createBoundField( 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); diff --git a/gson/src/main/java/com/google/gson/internal/bind/TreeTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/TreeTypeAdapter.java index a1a5f760d4..e3c7f0552b 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/TreeTypeAdapter.java +++ b/gson/src/main/java/com/google/gson/internal/bind/TreeTypeAdapter.java @@ -43,6 +43,10 @@ public final class TreeTypeAdapter extends SerializationDelegatingTypeAdapter private final JsonDeserializer deserializer; final Gson gson; private final TypeToken typeToken; + /** + * Only intended as {@code skipPast} for {@link Gson#getDelegateAdapter(TypeAdapterFactory, TypeToken)}, + * must not be used in any other way. + */ private final TypeAdapterFactory skipPast; private final GsonContextImpl context = new GsonContextImpl(); private final boolean nullSafe; diff --git a/gson/src/test/java/com/google/gson/GsonTest.java b/gson/src/test/java/com/google/gson/GsonTest.java index fcdc7cbc54..ddb5a93d61 100644 --- a/gson/src/test/java/com/google/gson/GsonTest.java +++ b/gson/src/test/java/com/google/gson/GsonTest.java @@ -271,6 +271,70 @@ public void run() { assertThat(otherThreadAdapter.get().toJson(null)).isEqualTo("[[\"wrapped-nested\"]]"); } + @Test + public void testGetDelegateAdapter() { + class DummyAdapter extends TypeAdapter { + 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 TypeAdapter create(Gson gson, TypeToken type) { + return (TypeAdapter) adapter; + } + } + + 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); + + // For unknown factory the first adapter for that type should be returned + assertThat(gson.getDelegateAdapter(new DummyFactory(new DummyAdapter(0)), type)).isEqualTo(adapter2); + + // For null as 'skipPast' the first adapter for that type should be returned + assertThat(gson.getDelegateAdapter(null, type)).isEqualTo(adapter2); + + assertThat(gson.getDelegateAdapter(factory2, type)).isEqualTo(adapter1); + // Default Gson adapter should be returned + assertThat(gson.getDelegateAdapter(factory1, type)).isNotInstanceOf(DummyAdapter.class); + } + @Test public void testNewJsonWriter_Default() throws IOException { StringWriter writer = new StringWriter(); diff --git a/gson/src/test/java/com/google/gson/functional/InstanceCreatorTest.java b/gson/src/test/java/com/google/gson/functional/InstanceCreatorTest.java index ea3f979983..842dd4cf83 100644 --- a/gson/src/test/java/com/google/gson/functional/InstanceCreatorTest.java +++ b/gson/src/test/java/com/google/gson/functional/InstanceCreatorTest.java @@ -17,6 +17,7 @@ package com.google.gson.functional; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -33,7 +34,7 @@ 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 @@ -129,4 +130,48 @@ class SubTreeSet extends TreeSet {} assertThat(set.first()).isEqualTo("b"); assertThat(set.getClass()).isEqualTo(SubTreeSet.class); } + + private static class CustomClass {} + + @Test + public void testInstanceCreatorReturnsNull() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(CustomClass.class, new InstanceCreator() { + @Override + public CustomClass createInstance(Type type) { + return null; + } + + @Override + public String toString() { + return "bad-instance-creator"; + } + }) + .create(); + + RuntimeException e = assertThrows(RuntimeException.class, () -> gson.fromJson("{}", CustomClass.class)); + assertThat(e).hasMessageThat().isEqualTo("InstanceCreator bad-instance-creator returned null for" + + " type class " + CustomClass.class.getName()); + } + + @Test + public void testInstanceCreatorReturnsWrongInstance() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(CustomClass.class, new InstanceCreator() { + @Override + public String createInstance(Type type) { + return "test"; + } + + @Override + public String toString() { + return "bad-instance-creator"; + } + }) + .create(); + + ClassCastException e = assertThrows(ClassCastException.class, () -> gson.fromJson("{}", CustomClass.class)); + assertThat(e).hasMessageThat().isEqualTo("InstanceCreator bad-instance-creator created instance of wrong type;" + + " expected " + CustomClass.class.getName() + " but got instance of unrelated type java.lang.String"); + } } diff --git a/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationOnClassesTest.java b/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationOnClassesTest.java index 3edd8d79d4..e5aa70f4f0 100644 --- a/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationOnClassesTest.java +++ b/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationOnClassesTest.java @@ -22,6 +22,7 @@ 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 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,331 @@ public void testIncorrectJsonAdapterType() { 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() { + WithDelegatingFactory deserialized = new Gson().fromJson("{\"custom\":{\"f\":\"de\"}}", WithDelegatingFactory.class); + assertThat(deserialized.f).isEqualTo("de"); + + WithDelegatingFactory serialized = new WithDelegatingFactory("se"); + assertThat(new Gson().toJson(serialized)).isEqualTo("{\"custom\":{\"f\":\"se\"}}"); + } + @JsonAdapter(WithDelegatingFactory.Factory.class) + private static class WithDelegatingFactory { + String f; + + WithDelegatingFactory(String f) { + this.f = f; + } + + static class Factory implements TypeAdapterFactory { + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + if (type.getRawType() != WithDelegatingFactory.class) { + return null; + } + + TypeAdapter delegate = gson.getDelegateAdapter(this, type); + + return new TypeAdapter() { + @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 TypeAdapter create(Gson gson, TypeToken type) { + return new TypeAdapter() { + @SuppressWarnings("SameNameButDifferent") // suppress Error Prone warning; should be clear that `Factory` refers to enclosing class + private TypeAdapter 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 different 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 serialized = new WithDelegatingFactory("se"); + assertThat(gson.toJson(serialized)).isEqualTo("{\"custom\":{\"custom\":{\"f\":\"se\"}}}"); + } + + /** + * Tests behavior of {@link Gson#getDelegateAdapter} when the same instance of a factory + * is used (through {@link InstanceCreator}). + * + *

Important: 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 serialized = new WithDelegatingFactory("se"); + assertThat(gson.toJson(serialized)).isEqualTo("{\"custom\":{\"f\":\"se\"}}"); + } + + /** + * Tests behavior of {@link Gson#getDelegateAdapter} when different 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. + * + *

Important: 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() { + @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 same 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. + * + *

Important: 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() { + @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 TypeAdapter create(Gson gson, TypeToken type) { + TypeAdapter delegate = gson.getDelegateAdapter(this, type); + + return new TypeAdapter() { + @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 { + @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 { + @Override + public WithJsonDeserializer deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { + return new WithJsonDeserializer("123"); + } + } + } } diff --git a/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationOnFieldsTest.java b/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationOnFieldsTest.java index b322ad8bff..226d3d6aa6 100644 --- a/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationOnFieldsTest.java +++ b/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationOnFieldsTest.java @@ -18,21 +18,32 @@ 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,335 @@ private static class Gizmo2PartTypeAdapterFactory implements TypeAdapterFactory }; } } + + /** + * 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 { + 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. + * + *

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 TypeAdapter create(Gson gson, TypeToken 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() { + WithDelegatingFactory deserialized = new Gson().fromJson("{\"f\":\"test\"}", WithDelegatingFactory.class); + assertThat(deserialized.f).isEqualTo("test-custom"); + + WithDelegatingFactory serialized = new WithDelegatingFactory(); + serialized.f = "value"; + assertThat(new Gson().toJson(serialized)).isEqualTo("{\"f\":\"value-custom\"}"); + } + private static class WithDelegatingFactory { + @SuppressWarnings("SameNameButDifferent") // suppress Error Prone warning; should be clear that `Factory` refers to nested class + @JsonAdapter(Factory.class) + String f; + + static class Factory implements TypeAdapterFactory { + @SuppressWarnings("unchecked") + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + TypeAdapter delegate = (TypeAdapter) gson.getDelegateAdapter(this, type); + + return (TypeAdapter) new TypeAdapter() { + @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 TypeAdapter create(Gson gson, TypeToken type) { + return (TypeAdapter) new TypeAdapter() { + private TypeAdapter delegate() { + return (TypeAdapter) 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 TypeAdapter create(Gson gson, TypeToken type) { + // Uses `Gson.getAdapter` instead of `Gson.getDelegateAdapter` + TypeAdapter delegate = (TypeAdapter) gson.getAdapter(type); + + return (TypeAdapter) new TypeAdapter() { + @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 f = Collections.emptyList(); + + static class Serializer implements JsonSerializer> { + @Override + public JsonElement serialize(List 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 f; + + WithJsonDeserializer(List f) { + this.f = f; + } + + static class Deserializer implements JsonDeserializer> { + @Override + public List deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { + return Arrays.asList(3, 2, 1); + } + } + } }