Skip to content

Commit 817a836

Browse files
committed
Consistent object type exposure for JSON rendering (workaround for Gson)
Issue: SPR-16461
1 parent 31b25b5 commit 817a836

File tree

6 files changed

+358
-122
lines changed

6 files changed

+358
-122
lines changed

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

Lines changed: 9 additions & 14 deletions
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.
@@ -87,21 +87,16 @@ public final void write(final T t, @Nullable final Type type, @Nullable MediaTyp
8787

8888
if (outputMessage instanceof StreamingHttpOutputMessage) {
8989
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
90-
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
90+
streamingOutputMessage.setBody(outputStream -> writeInternal(t, type, new HttpOutputMessage() {
9191
@Override
92-
public void writeTo(final OutputStream outputStream) throws IOException {
93-
writeInternal(t, type, new HttpOutputMessage() {
94-
@Override
95-
public OutputStream getBody() throws IOException {
96-
return outputStream;
97-
}
98-
@Override
99-
public HttpHeaders getHeaders() {
100-
return headers;
101-
}
102-
});
92+
public OutputStream getBody() {
93+
return outputStream;
10394
}
104-
});
95+
@Override
96+
public HttpHeaders getHeaders() {
97+
return headers;
98+
}
99+
}));
105100
}
106101
else {
107102
writeInternal(t, type, outputMessage);

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

Lines changed: 9 additions & 3 deletions
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.
@@ -18,6 +18,7 @@
1818

1919
import java.io.Reader;
2020
import java.io.Writer;
21+
import java.lang.reflect.ParameterizedType;
2122
import java.lang.reflect.Type;
2223

2324
import com.google.gson.Gson;
@@ -34,7 +35,7 @@
3435
* By default, it supports {@code application/json} and {@code application/*+json} with
3536
* {@code UTF-8} character set.
3637
*
37-
* <p>Tested against Gson 2.6; compatible with Gson 2.0 and higher.
38+
* <p>Tested against Gson 2.8; compatible with Gson 2.0 and higher.
3839
*
3940
* @author Roy Clarkson
4041
* @author Juergen Hoeller
@@ -93,7 +94,12 @@ protected Object readInternal(Type resolvedType, Reader reader) throws Exception
9394

9495
@Override
9596
protected void writeInternal(Object o, @Nullable Type type, Writer writer) throws Exception {
96-
if (type != null) {
97+
// In Gson, toJson with a type argument will exclusively use that given type,
98+
// ignoring the actual type of the object... which might be more specific,
99+
// e.g. a subclass of the specified type which includes additional fields.
100+
// As a consequence, we're only passing in parameterized type declarations
101+
// which might contain extra generics that the object instance doesn't retain.
102+
if (type instanceof ParameterizedType) {
97103
getGson().toJson(o, type, writer);
98104
}
99105
else {

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

Lines changed: 3 additions & 2 deletions
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.
@@ -18,6 +18,7 @@
1818

1919
import java.io.Reader;
2020
import java.io.Writer;
21+
import java.lang.reflect.ParameterizedType;
2122
import java.lang.reflect.Type;
2223
import javax.json.bind.Jsonb;
2324
import javax.json.bind.JsonbBuilder;
@@ -100,7 +101,7 @@ protected Object readInternal(Type resolvedType, Reader reader) throws Exception
100101

101102
@Override
102103
protected void writeInternal(Object o, @Nullable Type type, Writer writer) throws Exception {
103-
if (type != null) {
104+
if (type instanceof ParameterizedType) {
104105
getJsonb().toJson(o, type, writer);
105106
}
106107
else {

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

Lines changed: 96 additions & 33 deletions
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.
@@ -27,6 +27,7 @@
2727
import java.util.Map;
2828

2929
import org.junit.Test;
30+
import org.skyscreamer.jsonassert.JSONAssert;
3031

3132
import org.springframework.core.ParameterizedTypeReference;
3233
import org.springframework.http.MediaType;
@@ -40,6 +41,7 @@
4041
* Gson 2.x converter tests.
4142
*
4243
* @author Roy Clarkson
44+
* @author Juergen Hoeller
4345
*/
4446
public class GsonHttpMessageConverterTests {
4547

@@ -129,6 +131,29 @@ 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+
Charset utf8 = StandardCharsets.UTF_8;
146+
String result = outputMessage.getBodyAsString(utf8);
147+
assertTrue(result.contains("\"string\":\"Foo\""));
148+
assertTrue(result.contains("\"number\":42"));
149+
assertTrue(result.contains("fraction\":42.0"));
150+
assertTrue(result.contains("\"array\":[\"Foo\",\"Bar\"]"));
151+
assertTrue(result.contains("\"bool\":true"));
152+
assertTrue(result.contains("\"bytes\":[1,2]"));
153+
assertEquals("Invalid content-type", new MediaType("application", "json", utf8),
154+
outputMessage.getHeaders().getContentType());
155+
}
156+
132157
@Test
133158
public void writeUTF16() throws IOException {
134159
MediaType contentType = new MediaType("application", "json", StandardCharsets.UTF_16BE);
@@ -149,7 +174,7 @@ public void readInvalidJson() throws IOException {
149174

150175
@Test
151176
@SuppressWarnings("unchecked")
152-
public void readGenerics() throws Exception {
177+
public void readAndWriteGenerics() throws Exception {
153178
Field beansList = ListHolder.class.getField("listField");
154179

155180
String body = "[{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," +
@@ -164,14 +189,18 @@ public void readGenerics() throws Exception {
164189
assertEquals("Foo", result.getString());
165190
assertEquals(42, result.getNumber());
166191
assertEquals(42F, result.getFraction(), 0F);
167-
assertArrayEquals(new String[] { "Foo", "Bar" }, result.getArray());
192+
assertArrayEquals(new String[] {"Foo", "Bar"}, result.getArray());
168193
assertTrue(result.isBool());
169-
assertArrayEquals(new byte[] { 0x1, 0x2 }, result.getBytes());
194+
assertArrayEquals(new byte[] {0x1, 0x2}, result.getBytes());
195+
196+
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
197+
converter.write(results, genericType, new MediaType("application", "json"), outputMessage);
198+
JSONAssert.assertEquals(body, outputMessage.getBodyAsString(StandardCharsets.UTF_8), true);
170199
}
171200

172201
@Test
173202
@SuppressWarnings("unchecked")
174-
public void readParameterizedType() throws Exception {
203+
public void readAndWriteParameterizedType() throws Exception {
175204
ParameterizedTypeReference<List<MyBean>> beansList = new ParameterizedTypeReference<List<MyBean>>() {
176205
};
177206

@@ -186,32 +215,74 @@ public void readParameterizedType() throws Exception {
186215
assertEquals("Foo", result.getString());
187216
assertEquals(42, result.getNumber());
188217
assertEquals(42F, result.getFraction(), 0F);
189-
assertArrayEquals(new String[] { "Foo", "Bar" }, result.getArray());
218+
assertArrayEquals(new String[] {"Foo", "Bar"}, result.getArray());
190219
assertTrue(result.isBool());
191220
assertArrayEquals(new byte[] {0x1, 0x2}, result.getBytes());
221+
222+
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
223+
converter.write(results, beansList.getType(), new MediaType("application", "json"), outputMessage);
224+
JSONAssert.assertEquals(body, outputMessage.getBodyAsString(StandardCharsets.UTF_8), true);
192225
}
193226

194227
@Test
195-
public void prefixJson() throws Exception {
228+
@SuppressWarnings("unchecked")
229+
public void writeParameterizedBaseType() throws Exception {
230+
ParameterizedTypeReference<List<MyBean>> beansList = new ParameterizedTypeReference<List<MyBean>>() {};
231+
ParameterizedTypeReference<List<MyBase>> baseList = new ParameterizedTypeReference<List<MyBase>>() {};
232+
233+
String body = "[{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," +
234+
"\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}]";
235+
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8));
236+
inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
237+
238+
List<MyBean> results = (List<MyBean>) converter.read(beansList.getType(), null, inputMessage);
239+
assertEquals(1, results.size());
240+
MyBean result = results.get(0);
241+
assertEquals("Foo", result.getString());
242+
assertEquals(42, result.getNumber());
243+
assertEquals(42F, result.getFraction(), 0F);
244+
assertArrayEquals(new String[] {"Foo", "Bar"}, result.getArray());
245+
assertTrue(result.isBool());
246+
assertArrayEquals(new byte[] {0x1, 0x2}, result.getBytes());
247+
248+
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
249+
converter.write(results, baseList.getType(), new MediaType("application", "json"), outputMessage);
250+
JSONAssert.assertEquals(body, outputMessage.getBodyAsString(StandardCharsets.UTF_8), true);
251+
}
252+
253+
@Test
254+
public void prefixJson() throws IOException {
196255
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
197256
this.converter.setPrefixJson(true);
198257
this.converter.writeInternal("foo", null, outputMessage);
199258
assertEquals(")]}', \"foo\"", outputMessage.getBodyAsString(StandardCharsets.UTF_8));
200259
}
201260

202261
@Test
203-
public void prefixJsonCustom() throws Exception {
262+
public void prefixJsonCustom() throws IOException {
204263
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
205264
this.converter.setJsonPrefix(")))");
206265
this.converter.writeInternal("foo", null, outputMessage);
207266
assertEquals(")))\"foo\"", outputMessage.getBodyAsString(StandardCharsets.UTF_8));
208267
}
209268

210269

211-
public static class MyBean {
270+
public static class MyBase {
212271

213272
private String string;
214273

274+
public String getString() {
275+
return string;
276+
}
277+
278+
public void setString(String string) {
279+
this.string = string;
280+
}
281+
}
282+
283+
284+
public static class MyBean extends MyBase {
285+
215286
private int number;
216287

217288
private float fraction;
@@ -222,30 +293,6 @@ public static class MyBean {
222293

223294
private byte[] bytes;
224295

225-
public byte[] getBytes() {
226-
return bytes;
227-
}
228-
229-
public void setBytes(byte[] bytes) {
230-
this.bytes = bytes;
231-
}
232-
233-
public boolean isBool() {
234-
return bool;
235-
}
236-
237-
public void setBool(boolean bool) {
238-
this.bool = bool;
239-
}
240-
241-
public String getString() {
242-
return string;
243-
}
244-
245-
public void setString(String string) {
246-
this.string = string;
247-
}
248-
249296
public int getNumber() {
250297
return number;
251298
}
@@ -269,6 +316,22 @@ public String[] getArray() {
269316
public void setArray(String[] array) {
270317
this.array = array;
271318
}
319+
320+
public boolean isBool() {
321+
return bool;
322+
}
323+
324+
public void setBool(boolean bool) {
325+
this.bool = bool;
326+
}
327+
328+
public byte[] getBytes() {
329+
return bytes;
330+
}
331+
332+
public void setBytes(byte[] bytes) {
333+
this.bytes = bytes;
334+
}
272335
}
273336

274337

0 commit comments

Comments
 (0)