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 60675037da..6c6e82e2ad 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 @@ -23,6 +23,7 @@ import java.io.IOException; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; final class TypeAdapterRuntimeTypeWrapper extends TypeAdapter { private final Gson context; @@ -100,8 +101,21 @@ private static boolean isReflective(TypeAdapter typeAdapter) { /** Finds a compatible runtime type if it is more specific */ private static Type getRuntimeTypeIfMoreSpecific(Type type, Object value) { - if (value != null && (type instanceof Class || type instanceof TypeVariable)) { - type = value.getClass(); + if (value == null) { + return type; + } + + Type declaredType = type; + // A wildcard says nothing more than its upper bound, so `? extends Base` is decided the same + // way as `Base`. Note that the bound is only used to make this decision; when it is a + // parameterized type the declared type is kept, the same as for a plain parameterized type, + // because the runtime class would lose the type arguments. + if (declaredType instanceof WildcardType) { + declaredType = ((WildcardType) declaredType).getUpperBounds()[0]; + } + + if (declaredType instanceof Class || declaredType instanceof TypeVariable) { + return value.getClass(); } return type; } diff --git a/gson/src/test/java/com/google/gson/functional/MoreSpecificTypeSerializationTest.java b/gson/src/test/java/com/google/gson/functional/MoreSpecificTypeSerializationTest.java index 0702884143..037159c914 100644 --- a/gson/src/test/java/com/google/gson/functional/MoreSpecificTypeSerializationTest.java +++ b/gson/src/test/java/com/google/gson/functional/MoreSpecificTypeSerializationTest.java @@ -74,6 +74,55 @@ public void testMapOfSubclassFields() { assertThat(sub.get("s").getAsInt()).isEqualTo(3); } + @Test + public void testWildcardListOfSubclassFields() { + List list = new ArrayList<>(); + list.add(new Sub(2, 3)); + ClassWithWildcardContainersOfBaseFields target = + new ClassWithWildcardContainersOfBaseFields(list, null, null); + String json = gson.toJson(target); + assertThat(json).contains("{\"s\":3,\"b\":2}"); + } + + @Test + public void testWildcardMapOfSubclassFields() { + Map map = new HashMap<>(); + map.put("sub", new Sub(2, 3)); + ClassWithWildcardContainersOfBaseFields target = + new ClassWithWildcardContainersOfBaseFields(null, map, null); + JsonObject json = gson.toJsonTree(target).getAsJsonObject().get("map").getAsJsonObject(); + JsonObject sub = json.get("sub").getAsJsonObject(); + assertThat(sub.get("b").getAsInt()).isEqualTo(2); + assertThat(sub.get("s").getAsInt()).isEqualTo(3); + } + + /** A wildcard can also end up as the type of a field, through a resolved type variable. */ + @Test + public void testWildcardTypeVariableSubclassFields() { + Container container = new Container<>(new Sub(2, 3)); + ClassWithWildcardContainersOfBaseFields target = + new ClassWithWildcardContainersOfBaseFields(null, null, container); + JsonObject json = gson.toJsonTree(target).getAsJsonObject().get("container").getAsJsonObject(); + JsonObject sub = json.get("t").getAsJsonObject(); + assertThat(sub.get("b").getAsInt()).isEqualTo(2); + assertThat(sub.get("s").getAsInt()).isEqualTo(3); + } + + /** + * For a wildcard whose bound is a parameterized type, Gson has to stick to the declared type, the + * same way it does for a plain parameterized type. + */ + @Test + public void testWildcardListOfParameterizedSubclassFields() { + List> list = new ArrayList<>(); + list.add(new ParameterizedSub<>("two", "three")); + ClassWithWildcardContainerOfParameterizedBaseFields target = + new ClassWithWildcardContainerOfParameterizedBaseFields(list); + String json = gson.toJson(target); + assertThat(json).contains("{\"t\":\"two\"}"); + assertThat(json).doesNotContain("\"s\":"); + } + /** For parameterized type, Gson ignores the more-specific type and sticks to the declared type */ @Test public void testParameterizedSubclassFields() { @@ -153,6 +202,38 @@ private static class ClassWithContainersOfBaseFields { } } + private static class Container { + T t; + + Container(T t) { + this.t = t; + } + } + + private static class ClassWithWildcardContainersOfBaseFields { + Collection collection; + Map map; + Container container; + + ClassWithWildcardContainersOfBaseFields( + Collection collection, + Map map, + Container container) { + this.collection = collection; + this.map = map; + this.container = container; + } + } + + private static class ClassWithWildcardContainerOfParameterizedBaseFields { + Collection> collection; + + ClassWithWildcardContainerOfParameterizedBaseFields( + Collection> collection) { + this.collection = collection; + } + } + private static class ParameterizedBase { T t;