From 764286a779c6ba6041c3884cd739bb73b94c7019 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 20 Sep 2020 00:09:21 +0200 Subject: [PATCH 1/6] Fix TypeAdapterRuntimeTypeWrapper not detecting reflective TreeTypeAdapter Previously on serialization TypeAdapterRuntimeTypeWrapper preferred a TreeTypeAdapter without `serializer` which falls back to the reflective adapter. This behavior was incorrect because it caused the reflective adapter for a Base class to be used for serialization (indirectly as TreeTypeAdapter delegate) instead of using the reflective adapter for a Subclass extending Base. --- .../gson/internal/bind/TreeTypeAdapter.java | 12 ++ .../bind/TypeAdapterRuntimeTypeWrapper.java | 32 ++++- .../TypeAdapterRuntimeTypeWrapperTest.java | 126 ++++++++++++++++++ 3 files changed, 165 insertions(+), 5 deletions(-) create mode 100644 gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java 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 a5c6c5dcda..c3aac4622b 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 @@ -89,6 +89,18 @@ private TypeAdapter delegate() { : (delegate = gson.getDelegateAdapter(skipPast, typeToken)); } + /** + * Returns the type adapter which is used for serialization. Returns {@code this} + * if this {@code TreeTypeAdapter} has a {@link #serializer}; otherwise returns + * the delegate. + * + * @return the type adapter which is used for serialization. + */ + // Package-private for TypeAdapterRuntimeTypeWrapper + TypeAdapter getSerializingTypeAdapter() { + return serializer != null ? this : delegate(); + } + /** * Returns a new factory that will match each type against {@code exactType}. */ diff --git a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java index 2bf37ad0a0..04af5b07f0 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java +++ b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java @@ -54,10 +54,10 @@ public void write(JsonWriter out, T value) throws IOException { Type runtimeType = getRuntimeTypeIfMoreSpecific(type, value); if (runtimeType != type) { TypeAdapter runtimeTypeAdapter = context.getAdapter(TypeToken.get(runtimeType)); - if (!(runtimeTypeAdapter instanceof ReflectiveTypeAdapterFactory.Adapter)) { + if (!isReflective(runtimeTypeAdapter)) { // The user registered a type adapter for the runtime type, so we will use that chosen = runtimeTypeAdapter; - } else if (!(delegate instanceof ReflectiveTypeAdapterFactory.Adapter)) { + } else if (!isReflective(delegate)) { // The user registered a type adapter for Base class, so we prefer it over the // reflective type adapter for the runtime type chosen = delegate; @@ -69,12 +69,34 @@ public void write(JsonWriter out, T value) throws IOException { chosen.write(out, value); } + /** + * @param typeAdapter the type adapter to check. + * @return whether the type adapter uses reflection. + */ + private static boolean isReflective(TypeAdapter typeAdapter) { + if (typeAdapter instanceof ReflectiveTypeAdapterFactory.Adapter) { + return true; + } else if (typeAdapter instanceof TreeTypeAdapter) { + // Have to get actual serializing adapter for TreeTypeAdapter because + // TreeTypeAdapter without `serializer` might fall back to reflective + // type adapter + TreeTypeAdapter treeTypeAdapter = (TreeTypeAdapter) typeAdapter; + TypeAdapter serializingTypeAdapter = treeTypeAdapter.getSerializingTypeAdapter(); + if (serializingTypeAdapter == treeTypeAdapter) { + return false; + } else { + // Check TreeTypeAdapter delegate + return isReflective(serializingTypeAdapter); + } + } + return false; + } + /** * Finds a compatible runtime type if it is more specific */ - private Type getRuntimeTypeIfMoreSpecific(Type type, Object value) { - if (value != null - && (type == Object.class || type instanceof TypeVariable || type instanceof Class)) { + private static Type getRuntimeTypeIfMoreSpecific(Type type, Object value) { + if (value != null && (type instanceof Class || type instanceof TypeVariable)) { type = value.getClass(); } return type; diff --git a/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java b/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java new file mode 100644 index 0000000000..47ab96e4a9 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java @@ -0,0 +1,126 @@ +package com.google.gson.functional; + +import java.io.IOException; +import java.lang.reflect.Type; + +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.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import junit.framework.TestCase; + +public class TypeAdapterRuntimeTypeWrapperTest extends TestCase { + private static class Base { + } + private static class Subclass extends Base { + @SuppressWarnings("unused") + String f = "test"; + } + private static class Container { + @SuppressWarnings("unused") + Base b = new Subclass(); + } + private static class Deserializer implements JsonDeserializer { + @Override + public Base deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { + throw new UnsupportedOperationException(); + } + } + + /** + * When custom {@link JsonSerializer} is registered for Base should + * prefer that over reflective adapter for Subclass for serialization. + */ + public void testJsonSerializer() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Base.class, new JsonSerializer() { + @Override + public JsonElement serialize(Base src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive("serializer"); + } + }) + .create(); + String json = gson.toJson(new Container()); + assertEquals("{\"b\":\"serializer\"}", json); + } + + /** + * When only {@link JsonDeserializer} is registered for Base, then on + * serialization should prefer reflective adapter for Subclass since + * Base would use reflective adapter as delegate. + */ + public void testJsonDeserializer_ReflectiveSerializerDelegate() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Base.class, new Deserializer()) + .create(); + String json = gson.toJson(new Container()); + assertEquals("{\"b\":{\"f\":\"test\"}}", json); + } + + /** + * When {@link JsonDeserializer} with custom adapter as delegate is + * registered for Base, then on serialization should prefer custom adapter + * delegate for Base over reflective adapter for Subclass. + */ + public void testJsonDeserializer_CustomSerializerDelegate() { + Gson gson = new GsonBuilder() + // Register custom delegate + .registerTypeAdapter(Base.class, new TypeAdapter() { + @Override + public Base read(JsonReader in) throws IOException { + throw new UnsupportedOperationException(); + } + @Override + public void write(JsonWriter out, Base value) throws IOException { + out.value("custom delegate"); + } + }) + .registerTypeAdapter(Base.class, new Deserializer()) + .create(); + String json = gson.toJson(new Container()); + assertEquals("{\"b\":\"custom delegate\"}", json); + } + + /** + * When two (or more) {@link JsonDeserializer}s are registered for Base + * which eventually fall back to reflective adapter as delegate, then on + * serialization should prefer reflective adapter for Subclass. + */ + public void testJsonDeserializer_ReflectiveTreeSerializerDelegate() { + Gson gson = new GsonBuilder() + // Register delegate which itself falls back to reflective serialization + .registerTypeAdapter(Base.class, new Deserializer()) + .registerTypeAdapter(Base.class, new Deserializer()) + .create(); + String json = gson.toJson(new Container()); + assertEquals("{\"b\":{\"f\":\"test\"}}", json); + } + + /** + * When {@link JsonDeserializer} with {@link JsonSerializer} as delegate + * is registered for Base, then on serialization should prefer + * {@code JsonSerializer} over reflective adapter for Subclass. + */ + public void testJsonDeserializer_JsonSerializerDelegate() { + Gson gson = new GsonBuilder() + // Register JsonSerializer as delegate + .registerTypeAdapter(Base.class, new JsonSerializer() { + @Override + public JsonElement serialize(Base src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive("custom delegate"); + } + }) + .registerTypeAdapter(Base.class, new Deserializer()) + .create(); + String json = gson.toJson(new Container()); + assertEquals("{\"b\":\"custom delegate\"}", json); + } +} From 767c10e8302a5a01fbff0f8af7a36c6147b785cd Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sat, 20 Aug 2022 15:55:55 +0200 Subject: [PATCH 2/6] Address review feedback --- .../com/google/gson/internal/bind/TreeTypeAdapter.java | 4 +--- .../internal/bind/TypeAdapterRuntimeTypeWrapper.java | 10 +++++----- 2 files changed, 6 insertions(+), 8 deletions(-) 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 92fc53dc9e..bcc52883f4 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 @@ -101,8 +101,6 @@ private TypeAdapter delegate() { * Returns the type adapter which is used for serialization. Returns {@code this} * if this {@code TreeTypeAdapter} has a {@link #serializer}; otherwise returns * the delegate. - * - * @return the type adapter which is used for serialization. */ // Package-private for TypeAdapterRuntimeTypeWrapper TypeAdapter getSerializingTypeAdapter() { @@ -181,5 +179,5 @@ private final class GsonContextImpl implements JsonSerializationContext, JsonDes @Override public R deserialize(JsonElement json, Type typeOfT) throws JsonParseException { return (R) gson.fromJson(json, typeOfT); } - }; + } } diff --git a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java index 04af5b07f0..e3d77d5091 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java +++ b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java @@ -15,15 +15,14 @@ */ package com.google.gson.internal.bind; -import java.io.IOException; -import java.lang.reflect.Type; -import java.lang.reflect.TypeVariable; - import com.google.gson.Gson; import com.google.gson.TypeAdapter; 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.lang.reflect.TypeVariable; final class TypeAdapterRuntimeTypeWrapper extends TypeAdapter { private final Gson context; @@ -70,8 +69,9 @@ public void write(JsonWriter out, T value) throws IOException { } /** + * Returns whether the type adapter uses reflection. + * * @param typeAdapter the type adapter to check. - * @return whether the type adapter uses reflection. */ private static boolean isReflective(TypeAdapter typeAdapter) { if (typeAdapter instanceof ReflectiveTypeAdapterFactory.Adapter) { From f980e6d041b8b1cdc1e2b13865148a48beb1dd7a Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sat, 8 Oct 2022 18:11:21 +0200 Subject: [PATCH 3/6] Convert TypeAdapterRuntimeTypeWrapperTest to JUnit 4 test --- .../TypeAdapterRuntimeTypeWrapperTest.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java b/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java index 47ab96e4a9..0708402b18 100644 --- a/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java +++ b/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java @@ -1,7 +1,6 @@ package com.google.gson.functional; -import java.io.IOException; -import java.lang.reflect.Type; +import static org.junit.Assert.assertEquals; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -14,10 +13,11 @@ import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.lang.reflect.Type; +import org.junit.Test; -import junit.framework.TestCase; - -public class TypeAdapterRuntimeTypeWrapperTest extends TestCase { +public class TypeAdapterRuntimeTypeWrapperTest { private static class Base { } private static class Subclass extends Base { @@ -39,6 +39,7 @@ public Base deserialize(JsonElement json, Type typeOfT, JsonDeserializationConte * When custom {@link JsonSerializer} is registered for Base should * prefer that over reflective adapter for Subclass for serialization. */ + @Test public void testJsonSerializer() { Gson gson = new GsonBuilder() .registerTypeAdapter(Base.class, new JsonSerializer() { @@ -57,6 +58,7 @@ public JsonElement serialize(Base src, Type typeOfSrc, JsonSerializationContext * serialization should prefer reflective adapter for Subclass since * Base would use reflective adapter as delegate. */ + @Test public void testJsonDeserializer_ReflectiveSerializerDelegate() { Gson gson = new GsonBuilder() .registerTypeAdapter(Base.class, new Deserializer()) @@ -70,6 +72,7 @@ public void testJsonDeserializer_ReflectiveSerializerDelegate() { * registered for Base, then on serialization should prefer custom adapter * delegate for Base over reflective adapter for Subclass. */ + @Test public void testJsonDeserializer_CustomSerializerDelegate() { Gson gson = new GsonBuilder() // Register custom delegate @@ -94,6 +97,7 @@ public void write(JsonWriter out, Base value) throws IOException { * which eventually fall back to reflective adapter as delegate, then on * serialization should prefer reflective adapter for Subclass. */ + @Test public void testJsonDeserializer_ReflectiveTreeSerializerDelegate() { Gson gson = new GsonBuilder() // Register delegate which itself falls back to reflective serialization @@ -109,6 +113,7 @@ public void testJsonDeserializer_ReflectiveTreeSerializerDelegate() { * is registered for Base, then on serialization should prefer * {@code JsonSerializer} over reflective adapter for Subclass. */ + @Test public void testJsonDeserializer_JsonSerializerDelegate() { Gson gson = new GsonBuilder() // Register JsonSerializer as delegate From eb966633b087fedd1461882df25fdb632184793a Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sat, 8 Oct 2022 18:27:07 +0200 Subject: [PATCH 4/6] Prefer wrapped reflective adapter for serialization of subclass --- .../bind/TypeAdapterRuntimeTypeWrapper.java | 4 +- .../TypeAdapterRuntimeTypeWrapperTest.java | 42 +++++++++++++++++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java index 97b3ceeda6..5a05eda1eb 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java +++ b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java @@ -53,7 +53,9 @@ public void write(JsonWriter out, T value) throws IOException { if (runtimeType != type) { @SuppressWarnings("unchecked") TypeAdapter runtimeTypeAdapter = (TypeAdapter) context.getAdapter(TypeToken.get(runtimeType)); - if (!isReflective(runtimeTypeAdapter)) { + // For backward compatibility only check ReflectiveTypeAdapterFactory.Adapter here but not any other + // wrapping adapters, see https://github.com/google/gson/pull/1787#issuecomment-1222175189 + if (!(runtimeTypeAdapter instanceof ReflectiveTypeAdapterFactory.Adapter)) { // The user registered a type adapter for the runtime type, so we will use that chosen = runtimeTypeAdapter; } else if (!isReflective(delegate)) { diff --git a/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java b/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java index 0708402b18..c79eaf3bba 100644 --- a/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java +++ b/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java @@ -31,7 +31,7 @@ private static class Container { private static class Deserializer implements JsonDeserializer { @Override public Base deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { - throw new UnsupportedOperationException(); + throw new AssertionError("not needed for this test"); } } @@ -49,6 +49,7 @@ public JsonElement serialize(Base src, Type typeOfSrc, JsonSerializationContext } }) .create(); + String json = gson.toJson(new Container()); assertEquals("{\"b\":\"serializer\"}", json); } @@ -63,6 +64,7 @@ public void testJsonDeserializer_ReflectiveSerializerDelegate() { Gson gson = new GsonBuilder() .registerTypeAdapter(Base.class, new Deserializer()) .create(); + String json = gson.toJson(new Container()); assertEquals("{\"b\":{\"f\":\"test\"}}", json); } @@ -75,7 +77,7 @@ public void testJsonDeserializer_ReflectiveSerializerDelegate() { @Test public void testJsonDeserializer_CustomSerializerDelegate() { Gson gson = new GsonBuilder() - // Register custom delegate + // Register custom delegate .registerTypeAdapter(Base.class, new TypeAdapter() { @Override public Base read(JsonReader in) throws IOException { @@ -88,6 +90,7 @@ public void write(JsonWriter out, Base value) throws IOException { }) .registerTypeAdapter(Base.class, new Deserializer()) .create(); + String json = gson.toJson(new Container()); assertEquals("{\"b\":\"custom delegate\"}", json); } @@ -100,10 +103,11 @@ public void write(JsonWriter out, Base value) throws IOException { @Test public void testJsonDeserializer_ReflectiveTreeSerializerDelegate() { Gson gson = new GsonBuilder() - // Register delegate which itself falls back to reflective serialization + // Register delegate which itself falls back to reflective serialization .registerTypeAdapter(Base.class, new Deserializer()) .registerTypeAdapter(Base.class, new Deserializer()) .create(); + String json = gson.toJson(new Container()); assertEquals("{\"b\":{\"f\":\"test\"}}", json); } @@ -116,7 +120,7 @@ public void testJsonDeserializer_ReflectiveTreeSerializerDelegate() { @Test public void testJsonDeserializer_JsonSerializerDelegate() { Gson gson = new GsonBuilder() - // Register JsonSerializer as delegate + // Register JsonSerializer as delegate .registerTypeAdapter(Base.class, new JsonSerializer() { @Override public JsonElement serialize(Base src, Type typeOfSrc, JsonSerializationContext context) { @@ -125,7 +129,37 @@ public JsonElement serialize(Base src, Type typeOfSrc, JsonSerializationContext }) .registerTypeAdapter(Base.class, new Deserializer()) .create(); + String json = gson.toJson(new Container()); assertEquals("{\"b\":\"custom delegate\"}", json); } + + /** + * When a {@link JsonDeserializer} is registered for Subclass, and a custom + * {@link JsonSerializer} is registered for Base, then Gson should prefer + * the reflective adapter for Subclass for backward compatibility (see + * https://github.com/google/gson/pull/1787#issuecomment-1222175189) even + * though normally TypeAdapterRuntimeTypeWrapper should prefer the custom + * serializer for Base. + */ + @Test + public void testJsonDeserializer_SubclassBackwardCompatibility() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Subclass.class, new JsonDeserializer() { + @Override + public Subclass deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { + throw new AssertionError("not needed for this test"); + } + }) + .registerTypeAdapter(Base.class, new JsonSerializer() { + @Override + public JsonElement serialize(Base src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive("base"); + } + }) + .create(); + + String json = gson.toJson(new Container()); + assertEquals("{\"b\":{\"f\":\"test\"}}", json); + } } From dd199e805500b3c1515fc7b50984e1a296e99511 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sat, 8 Oct 2022 19:00:36 +0200 Subject: [PATCH 5/6] Detect reflective adapter used as delegate for Gson.FutureTypeAdapter --- gson/src/main/java/com/google/gson/Gson.java | 22 +++++++++------ .../SerializationDelegatingTypeAdapter.java | 14 ++++++++++ .../gson/internal/bind/TreeTypeAdapter.java | 5 ++-- .../bind/TypeAdapterRuntimeTypeWrapper.java | 23 ++++++--------- .../TypeAdapterRuntimeTypeWrapperTest.java | 28 +++++++++++++++++++ 5 files changed, 67 insertions(+), 25 deletions(-) create mode 100644 gson/src/main/java/com/google/gson/internal/bind/SerializationDelegatingTypeAdapter.java diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index bb3e2c7704..22071a17d8 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -32,6 +32,7 @@ import com.google.gson.internal.bind.NumberTypeAdapter; import com.google.gson.internal.bind.ObjectTypeAdapter; import com.google.gson.internal.bind.ReflectiveTypeAdapterFactory; +import com.google.gson.internal.bind.SerializationDelegatingTypeAdapter; import com.google.gson.internal.bind.TypeAdapters; import com.google.gson.internal.sql.SqlTypesSupport; import com.google.gson.reflect.TypeToken; @@ -1315,7 +1316,7 @@ public T fromJson(JsonElement json, TypeToken typeOfT) throws JsonSyntaxE return fromJson(new JsonTreeReader(json), typeOfT); } - static class FutureTypeAdapter extends TypeAdapter { + static class FutureTypeAdapter extends SerializationDelegatingTypeAdapter { private TypeAdapter delegate; public void setDelegate(TypeAdapter typeAdapter) { @@ -1325,18 +1326,23 @@ public void setDelegate(TypeAdapter typeAdapter) { delegate = typeAdapter; } - @Override public T read(JsonReader in) throws IOException { + private TypeAdapter delegate() { if (delegate == null) { - throw new IllegalStateException(); + throw new IllegalStateException("Delegate has not been set yet"); } - return delegate.read(in); + return delegate; + } + + @Override public TypeAdapter getSerializationDelegate() { + return delegate(); + } + + @Override public T read(JsonReader in) throws IOException { + return delegate().read(in); } @Override public void write(JsonWriter out, T value) throws IOException { - if (delegate == null) { - throw new IllegalStateException(); - } - delegate.write(out, value); + delegate().write(out, value); } } diff --git a/gson/src/main/java/com/google/gson/internal/bind/SerializationDelegatingTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/SerializationDelegatingTypeAdapter.java new file mode 100644 index 0000000000..dad4ff1120 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/bind/SerializationDelegatingTypeAdapter.java @@ -0,0 +1,14 @@ +package com.google.gson.internal.bind; + +import com.google.gson.TypeAdapter; + +/** + * Type adapter which might delegate serialization to another adapter. + */ +public abstract class SerializationDelegatingTypeAdapter extends TypeAdapter { + /** + * Returns the adapter used for serialization, might be {@code this} or another adapter. + * That other adapter might itself also be a {@code SerializationDelegatingTypeAdapter}. + */ + public abstract TypeAdapter getSerializationDelegate(); +} 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 bcc52883f4..560234c07c 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 @@ -38,7 +38,7 @@ * tree adapter may be serialization-only or deserialization-only, this class * has a facility to lookup a delegate type adapter on demand. */ -public final class TreeTypeAdapter extends TypeAdapter { +public final class TreeTypeAdapter extends SerializationDelegatingTypeAdapter { private final JsonSerializer serializer; private final JsonDeserializer deserializer; final Gson gson; @@ -102,8 +102,7 @@ private TypeAdapter delegate() { * if this {@code TreeTypeAdapter} has a {@link #serializer}; otherwise returns * the delegate. */ - // Package-private for TypeAdapterRuntimeTypeWrapper - TypeAdapter getSerializingTypeAdapter() { + @Override public TypeAdapter getSerializationDelegate() { return serializer != null ? this : delegate(); } diff --git a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java index 5a05eda1eb..56fd61d0f9 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java +++ b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java @@ -76,22 +76,17 @@ public void write(JsonWriter out, T value) throws IOException { * @param typeAdapter the type adapter to check. */ private static boolean isReflective(TypeAdapter typeAdapter) { - if (typeAdapter instanceof ReflectiveTypeAdapterFactory.Adapter) { - return true; - } else if (typeAdapter instanceof TreeTypeAdapter) { - // Have to get actual serializing adapter for TreeTypeAdapter because - // TreeTypeAdapter without `serializer` might fall back to reflective - // type adapter - TreeTypeAdapter treeTypeAdapter = (TreeTypeAdapter) typeAdapter; - TypeAdapter serializingTypeAdapter = treeTypeAdapter.getSerializingTypeAdapter(); - if (serializingTypeAdapter == treeTypeAdapter) { - return false; - } else { - // Check TreeTypeAdapter delegate - return isReflective(serializingTypeAdapter); + // Run this in loop in case multiple delegating adapters are nested + while(typeAdapter instanceof SerializationDelegatingTypeAdapter) { + TypeAdapter delegate = ((SerializationDelegatingTypeAdapter) typeAdapter).getSerializationDelegate(); + // Break if adapter does not delegate serialization + if (delegate == typeAdapter) { + break; } + typeAdapter = delegate; } - return false; + + return typeAdapter instanceof ReflectiveTypeAdapterFactory.Adapter; } /** diff --git a/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java b/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java index c79eaf3bba..73a0101243 100644 --- a/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java +++ b/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java @@ -162,4 +162,32 @@ public JsonElement serialize(Base src, Type typeOfSrc, JsonSerializationContext String json = gson.toJson(new Container()); assertEquals("{\"b\":{\"f\":\"test\"}}", json); } + + private static class CyclicBase { + @SuppressWarnings("unused") + CyclicBase f; + } + + private static class CyclicSub extends CyclicBase { + @SuppressWarnings("unused") + int i; + + public CyclicSub(int i) { + this.i = i; + } + } + + /** + * Tests behavior when the type of a field refers to a type whose adapter is + * currently in the process of being created. For these cases {@link Gson} + * uses a future adapter for the type. That adapter later uses the actual + * adapter as delegate. + */ + @Test + public void testGsonFutureAdapter() { + CyclicBase b = new CyclicBase(); + b.f = new CyclicSub(2); + String json = new Gson().toJson(b); + assertEquals("{\"f\":{\"i\":2}}", json); + } } From aeda2bd8244a42fcfa6b97bcb83b9d802c0ff49d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Mon, 10 Oct 2022 16:05:05 -0700 Subject: [PATCH 6/6] Tiny style tweak. --- .../gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java index 56fd61d0f9..75a991ead7 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java +++ b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java @@ -77,7 +77,7 @@ public void write(JsonWriter out, T value) throws IOException { */ private static boolean isReflective(TypeAdapter typeAdapter) { // Run this in loop in case multiple delegating adapters are nested - while(typeAdapter instanceof SerializationDelegatingTypeAdapter) { + while (typeAdapter instanceof SerializationDelegatingTypeAdapter) { TypeAdapter delegate = ((SerializationDelegatingTypeAdapter) typeAdapter).getSerializationDelegate(); // Break if adapter does not delegate serialization if (delegate == typeAdapter) {