Skip to content

Commit fd964ca

Browse files
committed
Consistent object type exposure for JSON rendering (workaround for Gson)
Issue: SPR-16461 (cherry picked from commit 817a836)
1 parent 5fd761e commit fd964ca

File tree

3 files changed

+253
-89
lines changed

3 files changed

+253
-89
lines changed

spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@
2020
import java.io.InputStreamReader;
2121
import java.io.OutputStreamWriter;
2222
import java.io.Reader;
23+
import java.lang.reflect.ParameterizedType;
2324
import java.lang.reflect.Type;
2425
import java.nio.charset.Charset;
2526

@@ -182,12 +183,19 @@ protected void writeInternal(Object o, Type type, HttpOutputMessage outputMessag
182183
if (this.jsonPrefix != null) {
183184
writer.append(this.jsonPrefix);
184185
}
185-
if (type != null) {
186+
187+
// In Gson, toJson with a type argument will exclusively use that given type,
188+
// ignoring the actual type of the object... which might be more specific,
189+
// e.g. a subclass of the specified type which includes additional fields.
190+
// As a consequence, we're only passing in parameterized type declarations
191+
// which might contain extra generics that the object instance doesn't retain.
192+
if (type instanceof ParameterizedType) {
186193
this.gson.toJson(o, type, writer);
187194
}
188195
else {
189196
this.gson.toJson(o, writer);
190197
}
198+
191199
writer.close();
192200
}
193201
catch (JsonIOException ex) {

spring-web/src/test/java/org/springframework/http/converter/json/GsonHttpMessageConverterTests.java

+107-40
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@
2626

2727
import com.google.gson.reflect.TypeToken;
2828
import org.junit.Test;
29+
import org.skyscreamer.jsonassert.JSONAssert;
2930

3031
import org.springframework.core.ParameterizedTypeReference;
3132
import org.springframework.http.MediaType;
@@ -39,12 +40,13 @@
3940
* Gson 2.x converter tests.
4041
*
4142
* @author Roy Clarkson
43+
* @author Juergen Hoeller
4244
*/
4345
public class GsonHttpMessageConverterTests {
4446

4547
private static final Charset UTF8 = Charset.forName("UTF-8");
4648

47-
private GsonHttpMessageConverter converter = new GsonHttpMessageConverter();
49+
private final GsonHttpMessageConverter converter = new GsonHttpMessageConverter();
4850

4951

5052
@Test
@@ -76,9 +78,9 @@ public void readTyped() throws IOException {
7678
assertEquals("Foo", result.getString());
7779
assertEquals(42, result.getNumber());
7880
assertEquals(42F, result.getFraction(), 0F);
79-
assertArrayEquals(new String[]{"Foo", "Bar"}, result.getArray());
81+
assertArrayEquals(new String[] {"Foo", "Bar"}, result.getArray());
8082
assertTrue(result.isBool());
81-
assertArrayEquals(new byte[]{0x1, 0x2}, result.getBytes());
83+
assertArrayEquals(new byte[] {0x1, 0x2}, result.getBytes());
8284
}
8385

8486
@Test
@@ -104,7 +106,7 @@ public void readUntyped() throws IOException {
104106
for (int i = 0; i < 2; i++) {
105107
bytes[i] = resultBytes.get(i).byteValue();
106108
}
107-
assertArrayEquals(new byte[]{0x1, 0x2}, bytes);
109+
assertArrayEquals(new byte[] {0x1, 0x2}, bytes);
108110
}
109111

110112
@Test
@@ -114,9 +116,9 @@ public void write() throws IOException {
114116
body.setString("Foo");
115117
body.setNumber(42);
116118
body.setFraction(42F);
117-
body.setArray(new String[]{"Foo", "Bar"});
119+
body.setArray(new String[] {"Foo", "Bar"});
118120
body.setBool(true);
119-
body.setBytes(new byte[]{0x1, 0x2});
121+
body.setBytes(new byte[] {0x1, 0x2});
120122
this.converter.write(body, null, outputMessage);
121123
String result = outputMessage.getBodyAsString(UTF8);
122124
assertTrue(result.contains("\"string\":\"Foo\""));
@@ -129,6 +131,28 @@ public void write() throws IOException {
129131
outputMessage.getHeaders().getContentType());
130132
}
131133

134+
@Test
135+
public void writeWithBaseType() throws IOException {
136+
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
137+
MyBean body = new MyBean();
138+
body.setString("Foo");
139+
body.setNumber(42);
140+
body.setFraction(42F);
141+
body.setArray(new String[] {"Foo", "Bar"});
142+
body.setBool(true);
143+
body.setBytes(new byte[] {0x1, 0x2});
144+
this.converter.write(body, MyBase.class, null, outputMessage);
145+
String result = outputMessage.getBodyAsString(UTF8);
146+
assertTrue(result.contains("\"string\":\"Foo\""));
147+
assertTrue(result.contains("\"number\":42"));
148+
assertTrue(result.contains("fraction\":42.0"));
149+
assertTrue(result.contains("\"array\":[\"Foo\",\"Bar\"]"));
150+
assertTrue(result.contains("\"bool\":true"));
151+
assertTrue(result.contains("\"bytes\":[1,2]"));
152+
assertEquals("Invalid content-type", new MediaType("application", "json", UTF8),
153+
outputMessage.getHeaders().getContentType());
154+
}
155+
132156
@Test
133157
public void writeUTF16() throws IOException {
134158
Charset utf16 = Charset.forName("UTF-16BE");
@@ -174,14 +198,14 @@ protected TypeToken<?> getTypeToken(Type type) {
174198
assertEquals("Foo", result.getString());
175199
assertEquals(42, result.getNumber());
176200
assertEquals(42F, result.getFraction(), 0F);
177-
assertArrayEquals(new String[] { "Foo", "Bar" }, result.getArray());
201+
assertArrayEquals(new String[] {"Foo", "Bar"}, result.getArray());
178202
assertTrue(result.isBool());
179-
assertArrayEquals(new byte[] { 0x1, 0x2 }, result.getBytes());
203+
assertArrayEquals(new byte[] {0x1, 0x2}, result.getBytes());
180204
}
181205

182206
@Test
183207
@SuppressWarnings("unchecked")
184-
public void readParameterizedType() throws IOException {
208+
public void readAndWriteParameterizedType() throws Exception {
185209
ParameterizedTypeReference<List<MyBean>> beansList = new ParameterizedTypeReference<List<MyBean>>() {
186210
};
187211

@@ -190,39 +214,80 @@ public void readParameterizedType() throws IOException {
190214
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(UTF8));
191215
inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
192216

193-
GsonHttpMessageConverter converter = new GsonHttpMessageConverter();
194217
List<MyBean> results = (List<MyBean>) converter.read(beansList.getType(), null, inputMessage);
195218
assertEquals(1, results.size());
196219
MyBean result = results.get(0);
197220
assertEquals("Foo", result.getString());
198221
assertEquals(42, result.getNumber());
199222
assertEquals(42F, result.getFraction(), 0F);
200-
assertArrayEquals(new String[] { "Foo", "Bar" }, result.getArray());
223+
assertArrayEquals(new String[] {"Foo", "Bar"}, result.getArray());
201224
assertTrue(result.isBool());
202-
assertArrayEquals(new byte[] { 0x1, 0x2 }, result.getBytes());
225+
assertArrayEquals(new byte[] {0x1, 0x2}, result.getBytes());
226+
227+
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
228+
converter.write(results, beansList.getType(), new MediaType("application", "json"), outputMessage);
229+
JSONAssert.assertEquals(body, outputMessage.getBodyAsString(UTF8), true);
203230
}
204231

205232
@Test
206-
public void prefixJson() throws Exception {
233+
@SuppressWarnings("unchecked")
234+
public void writeParameterizedBaseType() throws Exception {
235+
ParameterizedTypeReference<List<MyBean>> beansList = new ParameterizedTypeReference<List<MyBean>>() {};
236+
ParameterizedTypeReference<List<MyBase>> baseList = new ParameterizedTypeReference<List<MyBase>>() {};
237+
238+
String body = "[{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," +
239+
"\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}]";
240+
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(UTF8));
241+
inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
242+
243+
List<MyBean> results = (List<MyBean>) converter.read(beansList.getType(), null, inputMessage);
244+
assertEquals(1, results.size());
245+
MyBean result = results.get(0);
246+
assertEquals("Foo", result.getString());
247+
assertEquals(42, result.getNumber());
248+
assertEquals(42F, result.getFraction(), 0F);
249+
assertArrayEquals(new String[] {"Foo", "Bar"}, result.getArray());
250+
assertTrue(result.isBool());
251+
assertArrayEquals(new byte[] {0x1, 0x2}, result.getBytes());
252+
253+
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
254+
converter.write(results, baseList.getType(), new MediaType("application", "json"), outputMessage);
255+
JSONAssert.assertEquals(body, outputMessage.getBodyAsString(UTF8), true);
256+
}
257+
258+
@Test
259+
public void prefixJson() throws IOException {
207260
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
208261
this.converter.setPrefixJson(true);
209262
this.converter.writeInternal("foo", null, outputMessage);
210263
assertEquals(")]}', \"foo\"", outputMessage.getBodyAsString(UTF8));
211264
}
212265

213266
@Test
214-
public void prefixJsonCustom() throws Exception {
267+
public void prefixJsonCustom() throws IOException {
215268
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
216269
this.converter.setJsonPrefix(")))");
217270
this.converter.writeInternal("foo", null, outputMessage);
218271
assertEquals(")))\"foo\"", outputMessage.getBodyAsString(UTF8));
219272
}
220273

221274

222-
public static class MyBean {
275+
public static class MyBase {
223276

224277
private String string;
225278

279+
public String getString() {
280+
return string;
281+
}
282+
283+
public void setString(String string) {
284+
this.string = string;
285+
}
286+
}
287+
288+
289+
public static class MyBean extends MyBase {
290+
226291
private int number;
227292

228293
private float fraction;
@@ -233,30 +298,6 @@ public static class MyBean {
233298

234299
private byte[] bytes;
235300

236-
public byte[] getBytes() {
237-
return bytes;
238-
}
239-
240-
public void setBytes(byte[] bytes) {
241-
this.bytes = bytes;
242-
}
243-
244-
public boolean isBool() {
245-
return bool;
246-
}
247-
248-
public void setBool(boolean bool) {
249-
this.bool = bool;
250-
}
251-
252-
public String getString() {
253-
return string;
254-
}
255-
256-
public void setString(String string) {
257-
this.string = string;
258-
}
259-
260301
public int getNumber() {
261302
return number;
262303
}
@@ -280,6 +321,32 @@ public String[] getArray() {
280321
public void setArray(String[] array) {
281322
this.array = array;
282323
}
324+
325+
public boolean isBool() {
326+
return bool;
327+
}
328+
329+
public void setBool(boolean bool) {
330+
this.bool = bool;
331+
}
332+
333+
public byte[] getBytes() {
334+
return bytes;
335+
}
336+
337+
public void setBytes(byte[] bytes) {
338+
this.bytes = bytes;
339+
}
340+
}
341+
342+
343+
public static class ListHolder<E> {
344+
345+
public List<E> listField;
346+
}
347+
348+
349+
public static class MyBeanListHolder extends ListHolder<MyBean> {
283350
}
284351

285352
}

0 commit comments

Comments
 (0)