diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index 80aa12888e..7671e87bf0 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -387,6 +387,7 @@ public Gson() { this.jsonAdapterFactory = new JsonAdapterAnnotationTypeAdapterFactory(constructorConstructor); factories.add(jsonAdapterFactory); factories.add(TypeAdapters.ENUM_FACTORY); + factories.add(TypeAdapters.RAW_ENUM_FACTORY); factories.add( new ReflectiveTypeAdapterFactory( constructorConstructor, diff --git a/gson/src/main/java/com/google/gson/internal/$Gson$Types.java b/gson/src/main/java/com/google/gson/internal/$Gson$Types.java index fc6b1a27d8..ed85c2480f 100644 --- a/gson/src/main/java/com/google/gson/internal/$Gson$Types.java +++ b/gson/src/main/java/com/google/gson/internal/$Gson$Types.java @@ -145,11 +145,14 @@ public static Class getRawType(Type type) { Type componentType = ((GenericArrayType) type).getGenericComponentType(); return Array.newInstance(getRawType(componentType), 0).getClass(); - } else if (type instanceof TypeVariable) { - // we could use the variable's bounds, but that won't work if there are multiple. - // having a raw type that's more general than necessary is okay - return Object.class; - + } else if (type instanceof TypeVariable) { + TypeVariable typeVariable = (TypeVariable) type; + // approximate the raw type with the bound of type type variable, if there are + // multiple bounds, all we can do is picking the first one + Type[] bounds = typeVariable.getBounds(); + // Javadoc specifies some bound is always returned, Object if not specified + assert bounds.length > 0; + return getRawType(bounds[0]); } else if (type instanceof WildcardType) { Type[] bounds = ((WildcardType) type).getUpperBounds(); // Currently the JLS only permits one bound for wildcards so using first bound is safe 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..dbc5fc47c9 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 @@ -44,7 +44,7 @@ public final class ObjectTypeAdapter extends TypeAdapter { private final Gson gson; private final ToNumberStrategy toNumberStrategy; - private ObjectTypeAdapter(Gson gson, ToNumberStrategy toNumberStrategy) { + ObjectTypeAdapter(Gson gson, ToNumberStrategy toNumberStrategy) { this.gson = gson; this.toNumberStrategy = toNumberStrategy; } diff --git a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java index 5d4bd1b5a2..c7308dd7a4 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java +++ b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java @@ -24,6 +24,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSyntaxException; +import com.google.gson.ToNumberPolicy; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; import com.google.gson.annotations.SerializedName; @@ -1030,6 +1031,22 @@ public TypeAdapter create(Gson gson, TypeToken typeToken) { } }; + public static final TypeAdapterFactory RAW_ENUM_FACTORY = + new TypeAdapterFactory() { + @Override + public TypeAdapter create(Gson gson, TypeToken typeToken) { + Class rawType = typeToken.getRawType(); + if (rawType == Enum.class) { + @SuppressWarnings("unchecked") + TypeAdapter adapter = + (TypeAdapter) new ObjectTypeAdapter(gson, ToNumberPolicy.DOUBLE); + return adapter; + } else { + return null; + } + } + }; + public static TypeAdapterFactory newFactory( final TypeToken type, final TypeAdapter typeAdapter) { return new TypeAdapterFactory() { diff --git a/gson/src/test/java/com/google/gson/HandleRawEnumTest.java b/gson/src/test/java/com/google/gson/HandleRawEnumTest.java new file mode 100644 index 0000000000..0882d17674 --- /dev/null +++ b/gson/src/test/java/com/google/gson/HandleRawEnumTest.java @@ -0,0 +1,96 @@ +package com.google.gson.functional; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import org.junit.Before; +import org.junit.Test; + +/** + * Test processing raw enum types. + * + * @author sevcenko + */ +public class HandleRawEnumTest { + private Gson gson; + + @Before + public void setUp() throws Exception { + gson = new Gson(); + } + + public enum SomeEnum { + ONE + } + + public static class ClassWithRawEnum { + private final Enum anyEnum; + + public ClassWithRawEnum(Enum anyEnum) { + this.anyEnum = anyEnum; + } + + public Enum getAnyEnum() { + return anyEnum; + } + } + + public static class ClassWithTypedEnum> { + private final T someEnum; + + public ClassWithTypedEnum(T someEnum) { + this.someEnum = someEnum; + } + + public T getSomeEnum() { + return someEnum; + } + } + + public static class GroupClass { + + private final ClassWithTypedEnum field; + + public GroupClass(ClassWithTypedEnum field) { + this.field = field; + } + + public ClassWithTypedEnum getField() { + return field; + } + } + + @Test + public void handleRawEnumClass() { + // serializing raw enum is fine, but note that Adapters.ENUM_FACTORY cannot handle raw enums + // even for serialization! before #2563, this just failed because raw enum falled through + // ReflectiveTypeAdapterFactory, which fails to even search enum for fields + assertThat(gson.toJson(new ClassWithRawEnum(SomeEnum.ONE))).isEqualTo("{\"anyEnum\":\"ONE\"}"); + + // we can deserialize if the enum type is known + assertThat( + gson.fromJson( + "{\"someEnum\":\"ONE\"}", new TypeToken>() {}) + .getSomeEnum()) + .isEqualTo(SomeEnum.ONE); + + assertThat(gson.toJson(new GroupClass(new ClassWithTypedEnum<>(SomeEnum.ONE)))) + .isEqualTo("{\"field\":{\"someEnum\":\"ONE\"}}"); + + assertThat( + gson.fromJson("{\"field\":{\"someEnum\":\"ONE\"}}", GroupClass.class) + .getField() + .getSomeEnum()) + .isEqualTo(SomeEnum.ONE); + ; + try { + // but raw type cannot be deserialized + gson.fromJson("{\"anyEnum\":\"ONE\"}", new TypeToken() {}); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessageThat().contains("Can not set final java.lang.Enum field"); + } + } +} 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!"); + } +}