Fix TypeAdapterRuntimeTypeWrapper not detecting reflective TreeTypeAdapter and FutureTypeAdapter (#1787)

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

* Address review feedback

* Convert TypeAdapterRuntimeTypeWrapperTest to JUnit 4 test

* Prefer wrapped reflective adapter for serialization of subclass

* Detect reflective adapter used as delegate for Gson.FutureTypeAdapter

* Tiny style tweak.

Co-authored-by: Éamonn McManus <emcmanus@google.com>
This commit is contained in:
Marcono1234 2022-10-11 01:10:48 +02:00 committed by GitHub
parent 5269701679
commit 8451c1fa63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 256 additions and 14 deletions

View File

@ -32,6 +32,7 @@ import com.google.gson.internal.bind.MapTypeAdapterFactory;
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 final class Gson {
return fromJson(new JsonTreeReader(json), typeOfT);
}
static class FutureTypeAdapter<T> extends TypeAdapter<T> {
static class FutureTypeAdapter<T> extends SerializationDelegatingTypeAdapter<T> {
private TypeAdapter<T> delegate;
public void setDelegate(TypeAdapter<T> typeAdapter) {
@ -1325,18 +1326,23 @@ public final class Gson {
delegate = typeAdapter;
}
@Override public T read(JsonReader in) throws IOException {
private TypeAdapter<T> 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<T> 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);
}
}

View File

@ -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<T> extends TypeAdapter<T> {
/**
* 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<T> getSerializationDelegate();
}

View File

@ -38,7 +38,7 @@ import java.lang.reflect.Type;
* 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<T> extends TypeAdapter<T> {
public final class TreeTypeAdapter<T> extends SerializationDelegatingTypeAdapter<T> {
private final JsonSerializer<T> serializer;
private final JsonDeserializer<T> deserializer;
final Gson gson;
@ -97,6 +97,15 @@ public final class TreeTypeAdapter<T> extends TypeAdapter<T> {
: (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.
*/
@Override public TypeAdapter<T> getSerializationDelegate() {
return serializer != null ? this : delegate();
}
/**
* Returns a new factory that will match each type against {@code exactType}.
*/
@ -169,5 +178,5 @@ public final class TreeTypeAdapter<T> extends TypeAdapter<T> {
@Override public <R> R deserialize(JsonElement json, Type typeOfT) throws JsonParseException {
return (R) gson.fromJson(json, typeOfT);
}
};
}
}

View File

@ -53,10 +53,12 @@ final class TypeAdapterRuntimeTypeWrapper<T> extends TypeAdapter<T> {
if (runtimeType != type) {
@SuppressWarnings("unchecked")
TypeAdapter<T> runtimeTypeAdapter = (TypeAdapter<T>) context.getAdapter(TypeToken.get(runtimeType));
// 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 (!(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;
@ -68,12 +70,30 @@ final class TypeAdapterRuntimeTypeWrapper<T> extends TypeAdapter<T> {
chosen.write(out, value);
}
/**
* Returns whether the type adapter uses reflection.
*
* @param typeAdapter the type adapter to check.
*/
private static boolean isReflective(TypeAdapter<?> typeAdapter) {
// 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 typeAdapter instanceof ReflectiveTypeAdapterFactory.Adapter;
}
/**
* 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;

View File

@ -0,0 +1,193 @@
package com.google.gson.functional;
import static org.junit.Assert.assertEquals;
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 java.io.IOException;
import java.lang.reflect.Type;
import org.junit.Test;
public class TypeAdapterRuntimeTypeWrapperTest {
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<Base> {
@Override
public Base deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
throw new AssertionError("not needed for this test");
}
}
/**
* 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<Base>() {
@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.
*/
@Test
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.
*/
@Test
public void testJsonDeserializer_CustomSerializerDelegate() {
Gson gson = new GsonBuilder()
// Register custom delegate
.registerTypeAdapter(Base.class, new TypeAdapter<Base>() {
@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.
*/
@Test
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.
*/
@Test
public void testJsonDeserializer_JsonSerializerDelegate() {
Gson gson = new GsonBuilder()
// Register JsonSerializer as delegate
.registerTypeAdapter(Base.class, new JsonSerializer<Base>() {
@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);
}
/**
* 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<Subclass>() {
@Override
public Subclass deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
throw new AssertionError("not needed for this test");
}
})
.registerTypeAdapter(Base.class, new JsonSerializer<Base>() {
@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);
}
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);
}
}