Skip to content

Commit 5843050

Browse files
use the runtime type for a wildcard element type
a field like List<? extends Base> holding a Sub lost the fields that Sub adds , because getRuntimeTypeIfMoreSpecific only looked at the runtime type for a Class or a type variable , and ? extends Base is neither . so List<? extends Base> and List<Base> gave different json for the same value . a wildcard says nothing more than its upper bound , so this reads the bound and then decides the same way as before . when the bound is a parameterized type the declared type is kept , same as gson already does for a plain parameterized type , because the runtime class would drop the type arguments . closes #1870
1 parent b3f4ca2 commit 5843050

2 files changed

Lines changed: 97 additions & 2 deletions

File tree

gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.io.IOException;
2424
import java.lang.reflect.Type;
2525
import java.lang.reflect.TypeVariable;
26+
import java.lang.reflect.WildcardType;
2627

2728
final class TypeAdapterRuntimeTypeWrapper<T> extends TypeAdapter<T> {
2829
private final Gson context;
@@ -100,8 +101,21 @@ private static boolean isReflective(TypeAdapter<?> typeAdapter) {
100101

101102
/** Finds a compatible runtime type if it is more specific */
102103
private static Type getRuntimeTypeIfMoreSpecific(Type type, Object value) {
103-
if (value != null && (type instanceof Class<?> || type instanceof TypeVariable<?>)) {
104-
type = value.getClass();
104+
if (value == null) {
105+
return type;
106+
}
107+
108+
Type declaredType = type;
109+
// A wildcard says nothing more than its upper bound, so `? extends Base` is decided the same
110+
// way as `Base`. Note that the bound is only used to make this decision; when it is a
111+
// parameterized type the declared type is kept, the same as for a plain parameterized type,
112+
// because the runtime class would lose the type arguments.
113+
if (declaredType instanceof WildcardType) {
114+
declaredType = ((WildcardType) declaredType).getUpperBounds()[0];
115+
}
116+
117+
if (declaredType instanceof Class<?> || declaredType instanceof TypeVariable<?>) {
118+
return value.getClass();
105119
}
106120
return type;
107121
}

gson/src/test/java/com/google/gson/functional/MoreSpecificTypeSerializationTest.java

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,55 @@ public void testMapOfSubclassFields() {
7474
assertThat(sub.get("s").getAsInt()).isEqualTo(3);
7575
}
7676

77+
@Test
78+
public void testWildcardListOfSubclassFields() {
79+
List<Sub> list = new ArrayList<>();
80+
list.add(new Sub(2, 3));
81+
ClassWithWildcardContainersOfBaseFields target =
82+
new ClassWithWildcardContainersOfBaseFields(list, null, null);
83+
String json = gson.toJson(target);
84+
assertThat(json).contains("{\"s\":3,\"b\":2}");
85+
}
86+
87+
@Test
88+
public void testWildcardMapOfSubclassFields() {
89+
Map<String, Sub> map = new HashMap<>();
90+
map.put("sub", new Sub(2, 3));
91+
ClassWithWildcardContainersOfBaseFields target =
92+
new ClassWithWildcardContainersOfBaseFields(null, map, null);
93+
JsonObject json = gson.toJsonTree(target).getAsJsonObject().get("map").getAsJsonObject();
94+
JsonObject sub = json.get("sub").getAsJsonObject();
95+
assertThat(sub.get("b").getAsInt()).isEqualTo(2);
96+
assertThat(sub.get("s").getAsInt()).isEqualTo(3);
97+
}
98+
99+
/** A wildcard can also end up as the type of a field, through a resolved type variable. */
100+
@Test
101+
public void testWildcardTypeVariableSubclassFields() {
102+
Container<Sub> container = new Container<>(new Sub(2, 3));
103+
ClassWithWildcardContainersOfBaseFields target =
104+
new ClassWithWildcardContainersOfBaseFields(null, null, container);
105+
JsonObject json = gson.toJsonTree(target).getAsJsonObject().get("container").getAsJsonObject();
106+
JsonObject sub = json.get("t").getAsJsonObject();
107+
assertThat(sub.get("b").getAsInt()).isEqualTo(2);
108+
assertThat(sub.get("s").getAsInt()).isEqualTo(3);
109+
}
110+
111+
/**
112+
* For a wildcard whose bound is a parameterized type, Gson has to stick to the declared type, the
113+
* same way it does for a plain parameterized type.
114+
*/
115+
@Test
116+
public void testWildcardListOfParameterizedSubclassFields() {
117+
List<ParameterizedSub<String>> list = new ArrayList<>();
118+
list.add(new ParameterizedSub<>("two", "three"));
119+
ClassWithWildcardContainerOfParameterizedBaseFields target =
120+
new ClassWithWildcardContainerOfParameterizedBaseFields(list);
121+
String json = gson.toJson(target);
122+
assertThat(json).contains("{\"t\":\"two\"}");
123+
assertThat(json).doesNotContain("\"s\":");
124+
}
125+
77126
/** For parameterized type, Gson ignores the more-specific type and sticks to the declared type */
78127
@Test
79128
public void testParameterizedSubclassFields() {
@@ -153,6 +202,38 @@ private static class ClassWithContainersOfBaseFields {
153202
}
154203
}
155204

205+
private static class Container<T> {
206+
T t;
207+
208+
Container(T t) {
209+
this.t = t;
210+
}
211+
}
212+
213+
private static class ClassWithWildcardContainersOfBaseFields {
214+
Collection<? extends Base> collection;
215+
Map<String, ? extends Base> map;
216+
Container<? extends Base> container;
217+
218+
ClassWithWildcardContainersOfBaseFields(
219+
Collection<? extends Base> collection,
220+
Map<String, ? extends Base> map,
221+
Container<? extends Base> container) {
222+
this.collection = collection;
223+
this.map = map;
224+
this.container = container;
225+
}
226+
}
227+
228+
private static class ClassWithWildcardContainerOfParameterizedBaseFields {
229+
Collection<? extends ParameterizedBase<String>> collection;
230+
231+
ClassWithWildcardContainerOfParameterizedBaseFields(
232+
Collection<? extends ParameterizedBase<String>> collection) {
233+
this.collection = collection;
234+
}
235+
}
236+
156237
private static class ParameterizedBase<T> {
157238
T t;
158239

0 commit comments

Comments
 (0)