From 7ee5ad6cd1640d957f3a73d89a271ee2ef9c7956 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Wed, 23 Aug 2023 02:15:18 +0200 Subject: [PATCH] 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. --- gson/src/main/java/com/google/gson/Gson.java | 96 +++-- .../java/com/google/gson/InstanceCreator.java | 4 +- ...onAdapterAnnotationTypeAdapterFactory.java | 118 +++++- .../bind/ReflectiveTypeAdapterFactory.java | 2 +- .../gson/internal/bind/TreeTypeAdapter.java | 15 +- .../test/java/com/google/gson/GsonTest.java | 84 +++++ .../gson/functional/InstanceCreatorTest.java | 2 +- .../JsonAdapterAnnotationOnClassesTest.java | 334 ++++++++++++++++- .../JsonAdapterAnnotationOnFieldsTest.java | 348 +++++++++++++++++- 9 files changed, 945 insertions(+), 58 deletions(-) diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index 0219c1a1..4ffda1ee 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; @@ -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: *

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 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. + * + *

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 this (the type adapter * factory from where {@code getDelegateAdapter} method is being invoked). @@ -648,9 +657,10 @@ public final class Gson { * @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)) { + 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); + } } /** diff --git a/gson/src/main/java/com/google/gson/InstanceCreator.java b/gson/src/main/java/com/google/gson/InstanceCreator.java index d5096a07..b973da07 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 @@ import java.lang.reflect.Type; * * *

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/bind/JsonAdapterAnnotationTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/JsonAdapterAnnotationTypeAdapterFactory.java index 9cd5649e..b444a4bd 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.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 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_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 + 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); + } + + // 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)` 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; + } } 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 6a43b817..d981f2c5 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 @@ 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); 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 a1a5f760..f4d6eedc 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,11 +43,18 @@ public final class TreeTypeAdapter extends SerializationDelegatingTypeAdapter private final JsonDeserializer deserializer; final Gson gson; private final TypeToken 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 delegate; public TreeTypeAdapter(JsonSerializer serializer, JsonDeserializer deserializer, @@ -56,7 +63,7 @@ public final class TreeTypeAdapter 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 extends SerializationDelegatingTypeAdapter TypeAdapter d = delegate; return d != null ? d - : (delegate = gson.getDelegateAdapter(skipPast, typeToken)); + : (delegate = gson.getDelegateAdapter(skipPastForGetDelegateAdapter, typeToken)); } /** diff --git a/gson/src/test/java/com/google/gson/GsonTest.java b/gson/src/test/java/com/google/gson/GsonTest.java index a8e8e88b..47c3836d 100644 --- a/gson/src/test/java/com/google/gson/GsonTest.java +++ b/gson/src/test/java/com/google/gson/GsonTest.java @@ -272,6 +272,90 @@ public final class GsonTest { 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; + } + + // 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(); 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 ea3f9799..e228ef15 100644 --- a/gson/src/test/java/com/google/gson/functional/InstanceCreatorTest.java +++ b/gson/src/test/java/com/google/gson/functional/InstanceCreatorTest.java @@ -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 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 3edd8d79..d540f6e7 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 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 deserialized = new Gson().fromJson("{\"custom\":{\"f\":\"de\"}}", WithDelegatingFactory.class); + assertThat(deserialized.f).isEqualTo("de"); + + deserialized = new Gson().fromJson("{\"custom\":{\"f\":\"de\"}}", new TypeToken>() {}); + 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 { + T f; + + WithDelegatingFactory(T 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 b322ad8b..6601b2d3 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 @@ 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 { + 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() { + @SuppressWarnings("unchecked") + WithDelegatingFactory deserialized = new Gson().fromJson("{\"f\":\"test\"}", WithDelegatingFactory.class); + assertThat(deserialized.f).isEqualTo("test-custom"); + + deserialized = new Gson().fromJson("{\"f\":\"test\"}", new TypeToken>() {}); + 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) + T 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); + } + } + } }