From 1b9c56554964d2e70a9e310c1b3e864bc891ba2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0ev=C4=8Denko?= Date: Thu, 7 Dec 2023 07:59:13 +0100 Subject: [PATCH 01/11] Approximate raw type with bound of type variable Fixes https://github.com/google/gson/issues/2563 --- .../java/com/google/gson/internal/$Gson$Types.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) 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 From e0628ef3e77b851e87e0df059d38cb995c65feee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0ev=C4=8Denko?= Date: Thu, 7 Dec 2023 08:11:19 +0100 Subject: [PATCH 02/11] spotless update From 266043a1338367d921bb99472263ae77ab8e6473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0ev=C4=8Denko?= Date: Thu, 7 Dec 2023 08:34:36 +0100 Subject: [PATCH 03/11] Unit test for type variable inference test. See https://github.com/google/gson/issues/2563 --- .../InferenceFromTypeVariableTest.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 gson/src/test/java/com/google/gson/functional/InferenceFromTypeVariableTest.java 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..b727ed9b9a --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/InferenceFromTypeVariableTest.java @@ -0,0 +1,54 @@ +package com.google.gson.functional; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +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 Gson(); + } + + 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; + } + } + + @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!"); + } +} From 7ba5de13c5bd99024f4f352be4b464ddc3b73a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0ev=C4=8Denko?= Date: Thu, 7 Dec 2023 10:19:50 +0100 Subject: [PATCH 04/11] Unit test method updated to testGenericWrapperWithBoundDeserialization --- .../google/gson/functional/InferenceFromTypeVariableTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gson/src/test/java/com/google/gson/functional/InferenceFromTypeVariableTest.java b/gson/src/test/java/com/google/gson/functional/InferenceFromTypeVariableTest.java index b727ed9b9a..6384557ac4 100644 --- a/gson/src/test/java/com/google/gson/functional/InferenceFromTypeVariableTest.java +++ b/gson/src/test/java/com/google/gson/functional/InferenceFromTypeVariableTest.java @@ -44,7 +44,7 @@ public T getFoo() { } @Test - public void testSubClassSerialization() { + public void testGenericWrapperWithBoundDeserialization() { 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 From 3bde53e7db0afdd404862b69db29f2f32b8c48ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0ev=C4=8Denko?= Date: Fri, 8 Dec 2023 11:21:56 +0100 Subject: [PATCH 05/11] HandleRawEnumTest - test serialization of raw enum type --- .../com/google/gson/HandleRawEnumTest.java | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 gson/src/test/java/com/google/gson/HandleRawEnumTest.java 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"); + } + } +} From afd1e7430c9469b2ab07f00a8510f4d62b57c054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0ev=C4=8Denko?= Date: Fri, 8 Dec 2023 11:23:03 +0100 Subject: [PATCH 06/11] ObjectTypeAdapter now handles raw enum type --- .../com/google/gson/internal/bind/ObjectTypeAdapter.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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..1faeb31aa3 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 @@ -54,7 +54,13 @@ private static TypeAdapterFactory newFactory(final ToNumberStrategy toNumberStra @SuppressWarnings("unchecked") @Override public TypeAdapter create(Gson gson, TypeToken type) { - if (type.getRawType() == Object.class) { + // ObjectTypeAdapter now handles also raw enums. This is fine for serialization, as + // TypeAdapterRuntimeTypeWrapper would be used. + // Prior #2563, ObjectTypeAdapter was sometimes used implicitly by the rule + // that bound of type variable was ignored and interpreted as Object type; + // by rectifying this rule, we need to explicitly define that ObjectTypeAdapter handles + // raw enums. + if (type.getRawType() == Object.class || type.getRawType() == Enum.class) { return (TypeAdapter) new ObjectTypeAdapter(gson, toNumberStrategy); } return null; From 0f1f14196cdb362a900ccd1d86a3c7c584bf4fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0ev=C4=8Denko?= Date: Fri, 8 Dec 2023 11:32:35 +0100 Subject: [PATCH 07/11] spotless update --- .../gson/functional/InferenceFromTypeVariableTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gson/src/test/java/com/google/gson/functional/InferenceFromTypeVariableTest.java b/gson/src/test/java/com/google/gson/functional/InferenceFromTypeVariableTest.java index 6384557ac4..8cca1e413e 100644 --- a/gson/src/test/java/com/google/gson/functional/InferenceFromTypeVariableTest.java +++ b/gson/src/test/java/com/google/gson/functional/InferenceFromTypeVariableTest.java @@ -4,13 +4,15 @@ import com.google.gson.Gson; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; /** * Test deserialization of generic wrapper with type bound. - * + * * @author sevcenko */ +@Ignore public class InferenceFromTypeVariableTest { private Gson gson; @@ -44,7 +46,7 @@ public T getFoo() { } @Test - public void testGenericWrapperWithBoundDeserialization() { + 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 From 13d2e496a18877e369a186a1b075ffa2485e8b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0ev=C4=8Denko?= Date: Fri, 8 Dec 2023 12:11:22 +0100 Subject: [PATCH 08/11] Overridable binding of raw Enum to ObjectTypeAdapter --- gson/src/main/java/com/google/gson/Gson.java | 1 + .../gson/internal/bind/ObjectTypeAdapter.java | 4 ++-- .../google/gson/internal/bind/TypeAdapters.java | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) 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/bind/ObjectTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java index 1faeb31aa3..e341337ce2 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; } @@ -60,7 +60,7 @@ public TypeAdapter create(Gson gson, TypeToken type) { // that bound of type variable was ignored and interpreted as Object type; // by rectifying this rule, we need to explicitly define that ObjectTypeAdapter handles // raw enums. - if (type.getRawType() == Object.class || type.getRawType() == Enum.class) { + if (type.getRawType() == Object.class) { return (TypeAdapter) new ObjectTypeAdapter(gson, toNumberStrategy); } return null; 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..c613dc6e6a 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,20 @@ 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) { + return (TypeAdapter) + new ObjectTypeAdapter(gson, ToNumberPolicy.DOUBLE); + } else { + return null; + } + } + }; + public static TypeAdapterFactory newFactory( final TypeToken type, final TypeAdapter typeAdapter) { return new TypeAdapterFactory() { From 9b69d1b7dd1eeecba27d17c5bf99747ac132f4ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0ev=C4=8Denko?= Date: Fri, 8 Dec 2023 12:25:15 +0100 Subject: [PATCH 09/11] fixed warnings --- .../java/com/google/gson/internal/bind/TypeAdapters.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 c613dc6e6a..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 @@ -1037,8 +1037,10 @@ public TypeAdapter create(Gson gson, TypeToken typeToken) { public TypeAdapter create(Gson gson, TypeToken typeToken) { Class rawType = typeToken.getRawType(); if (rawType == Enum.class) { - return (TypeAdapter) - new ObjectTypeAdapter(gson, ToNumberPolicy.DOUBLE); + @SuppressWarnings("unchecked") + TypeAdapter adapter = + (TypeAdapter) new ObjectTypeAdapter(gson, ToNumberPolicy.DOUBLE); + return adapter; } else { return null; } From 3cad07c3b8dae35c472aba6133e561f01654a0f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0ev=C4=8Denko?= Date: Fri, 8 Dec 2023 13:16:59 +0100 Subject: [PATCH 10/11] remove extra comment --- .../com/google/gson/internal/bind/ObjectTypeAdapter.java | 6 ------ 1 file changed, 6 deletions(-) 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 e341337ce2..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 @@ -54,12 +54,6 @@ private static TypeAdapterFactory newFactory(final ToNumberStrategy toNumberStra @SuppressWarnings("unchecked") @Override public TypeAdapter create(Gson gson, TypeToken type) { - // ObjectTypeAdapter now handles also raw enums. This is fine for serialization, as - // TypeAdapterRuntimeTypeWrapper would be used. - // Prior #2563, ObjectTypeAdapter was sometimes used implicitly by the rule - // that bound of type variable was ignored and interpreted as Object type; - // by rectifying this rule, we need to explicitly define that ObjectTypeAdapter handles - // raw enums. if (type.getRawType() == Object.class) { return (TypeAdapter) new ObjectTypeAdapter(gson, toNumberStrategy); } From b9f96f5fec7f9f718589da534903b3ae0c7efdf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0ev=C4=8Denko?= Date: Mon, 11 Dec 2023 12:00:45 +0100 Subject: [PATCH 11/11] spotless --- .../InferenceFromTypeVariableTest.java | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/gson/src/test/java/com/google/gson/functional/InferenceFromTypeVariableTest.java b/gson/src/test/java/com/google/gson/functional/InferenceFromTypeVariableTest.java index 8cca1e413e..490da5a214 100644 --- a/gson/src/test/java/com/google/gson/functional/InferenceFromTypeVariableTest.java +++ b/gson/src/test/java/com/google/gson/functional/InferenceFromTypeVariableTest.java @@ -3,8 +3,13 @@ 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.Ignore; import org.junit.Test; /** @@ -12,13 +17,12 @@ * * @author sevcenko */ -@Ignore public class InferenceFromTypeVariableTest { private Gson gson; @Before public void setUp() throws Exception { - gson = new Gson(); + gson = new GsonBuilder().registerTypeAdapterFactory(new ResolveGenericBoundFactory()).create(); } public static class Foo { @@ -45,6 +49,23 @@ public T getFoo() { } } + 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!"));