diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index 80aa12888e..1bbfd3189e 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -324,7 +324,7 @@ public Gson() { // built-in type adapters that cannot be overridden factories.add(TypeAdapters.JSON_ELEMENT_FACTORY); - factories.add(ObjectTypeAdapter.getFactory(objectToNumberStrategy)); + factories.add(ObjectTypeAdapter.getFactory(objectToNumberStrategy, true)); // the excluder must precede all adapters that handle user-defined types factories.add(excluder); @@ -332,6 +332,8 @@ public Gson() { // users' type adapters factories.addAll(factoriesToBeAdded); + factories.add(ObjectTypeAdapter.getFactory(objectToNumberStrategy, false)); + // type adapters for basic platform types factories.add(TypeAdapters.STRING_FACTORY); factories.add(TypeAdapters.INTEGER_FACTORY); diff --git a/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java index 20d1606291..541ce8c683 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java +++ b/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java @@ -27,6 +27,8 @@ import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import java.io.IOException; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; @@ -39,7 +41,7 @@ */ public final class ObjectTypeAdapter extends TypeAdapter { /** Gson default factory using {@link ToNumberPolicy#DOUBLE}. */ - private static final TypeAdapterFactory DOUBLE_FACTORY = newFactory(ToNumberPolicy.DOUBLE); + private static final TypeAdapterFactory DOUBLE_FACTORY = newFactory(ToNumberPolicy.DOUBLE, true); private final Gson gson; private final ToNumberStrategy toNumberStrategy; @@ -49,24 +51,41 @@ private ObjectTypeAdapter(Gson gson, ToNumberStrategy toNumberStrategy) { this.toNumberStrategy = toNumberStrategy; } - private static TypeAdapterFactory newFactory(final ToNumberStrategy toNumberStrategy) { + private static TypeAdapterFactory newFactory( + final ToNumberStrategy toNumberStrategy, final boolean skipTypeVariable) { return new TypeAdapterFactory() { @SuppressWarnings("unchecked") @Override public TypeAdapter create(Gson gson, TypeToken type) { - if (type.getRawType() == Object.class) { + if (type.getRawType() == Object.class + && (!skipTypeVariable || !isTypeVariableWithBound(type.getType()))) { return (TypeAdapter) new ObjectTypeAdapter(gson, toNumberStrategy); } return null; } + + private boolean isTypeVariableWithBound(Type type) { + if (type instanceof TypeVariable) { + TypeVariable tv = (TypeVariable) type; + Type bound = tv.getBounds()[0]; + return bound != Object.class && bound instanceof Class; + } else { + return false; + } + } }; } public static TypeAdapterFactory getFactory(ToNumberStrategy toNumberStrategy) { + return getFactory(toNumberStrategy, false); + } + + public static TypeAdapterFactory getFactory( + ToNumberStrategy toNumberStrategy, boolean skipTypeVariable) { if (toNumberStrategy == ToNumberPolicy.DOUBLE) { return DOUBLE_FACTORY; } else { - return newFactory(toNumberStrategy); + return newFactory(toNumberStrategy, skipTypeVariable); } } diff --git a/gson/src/test/java/com/google/gson/functional/InferenceFromTypeVariableTest.java b/gson/src/test/java/com/google/gson/functional/InferenceFromTypeVariableTest.java new file mode 100644 index 0000000000..490da5a214 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/InferenceFromTypeVariableTest.java @@ -0,0 +1,77 @@ +package com.google.gson.functional; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import org.junit.Before; +import org.junit.Test; + +/** + * Test deserialization of generic wrapper with type bound. + * + * @author sevcenko + */ +public class InferenceFromTypeVariableTest { + private Gson gson; + + @Before + public void setUp() throws Exception { + gson = new GsonBuilder().registerTypeAdapterFactory(new ResolveGenericBoundFactory()).create(); + } + + public static class Foo { + private final String text; + + public Foo(String text) { + this.text = text; + } + + public String getText() { + return text; + } + } + + public static class BarDynamic { + private final T foo; + + public BarDynamic(T foo) { + this.foo = foo; + } + + public T getFoo() { + return foo; + } + } + + static class ResolveGenericBoundFactory implements TypeAdapterFactory { + + @SuppressWarnings("unchecked") + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + if (type.getType() instanceof TypeVariable) { + TypeVariable tv = (TypeVariable) type.getType(); + Type[] bounds = tv.getBounds(); + if (bounds.length == 1 && bounds[0] != Object.class) { + Type bound = bounds[0]; + return (TypeAdapter) gson.getAdapter(TypeToken.get(bound)); + } + } + return null; + } + } + + @Test + public void testSubClassSerialization() { + BarDynamic bar = new BarDynamic<>(new Foo("foo!")); + assertThat(gson.toJson(bar)).isEqualTo("{\"foo\":{\"text\":\"foo!\"}}"); + // without #2563 fix, this would deserialize foo as Object and fails to assign it to foo field + BarDynamic deserialized = gson.fromJson(gson.toJson(bar), BarDynamic.class); + assertThat(deserialized.getFoo().getText()).isEqualTo("foo!"); + } +}