From fd964ca4b11823e4d63f3691cb159ea9e4988fe5 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 5 Feb 2018 22:51:33 +0100 Subject: [PATCH] Consistent object type exposure for JSON rendering (workaround for Gson) Issue: SPR-16461 (cherry picked from commit 817a836) --- .../json/GsonHttpMessageConverter.java | 12 +- .../json/GsonHttpMessageConverterTests.java | 147 ++++++++++---- ...pingJackson2HttpMessageConverterTests.java | 183 +++++++++++++----- 3 files changed, 253 insertions(+), 89 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java index 744230cacb..3494155e9d 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.nio.charset.Charset; @@ -182,12 +183,19 @@ public class GsonHttpMessageConverter extends AbstractGenericHttpMessageConverte if (this.jsonPrefix != null) { writer.append(this.jsonPrefix); } - if (type != null) { + + // In Gson, toJson with a type argument will exclusively use that given type, + // ignoring the actual type of the object... which might be more specific, + // e.g. a subclass of the specified type which includes additional fields. + // As a consequence, we're only passing in parameterized type declarations + // which might contain extra generics that the object instance doesn't retain. + if (type instanceof ParameterizedType) { this.gson.toJson(o, type, writer); } else { this.gson.toJson(o, writer); } + writer.close(); } catch (JsonIOException ex) { diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/GsonHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/GsonHttpMessageConverterTests.java index 5365b943a1..8b07581888 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/GsonHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/GsonHttpMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import java.util.Map; import com.google.gson.reflect.TypeToken; import org.junit.Test; +import org.skyscreamer.jsonassert.JSONAssert; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; @@ -39,12 +40,13 @@ import static org.junit.Assert.*; * Gson 2.x converter tests. * * @author Roy Clarkson + * @author Juergen Hoeller */ public class GsonHttpMessageConverterTests { private static final Charset UTF8 = Charset.forName("UTF-8"); - private GsonHttpMessageConverter converter = new GsonHttpMessageConverter(); + private final GsonHttpMessageConverter converter = new GsonHttpMessageConverter(); @Test @@ -76,9 +78,9 @@ public class GsonHttpMessageConverterTests { assertEquals("Foo", result.getString()); assertEquals(42, result.getNumber()); assertEquals(42F, result.getFraction(), 0F); - assertArrayEquals(new String[]{"Foo", "Bar"}, result.getArray()); + assertArrayEquals(new String[] {"Foo", "Bar"}, result.getArray()); assertTrue(result.isBool()); - assertArrayEquals(new byte[]{0x1, 0x2}, result.getBytes()); + assertArrayEquals(new byte[] {0x1, 0x2}, result.getBytes()); } @Test @@ -104,7 +106,7 @@ public class GsonHttpMessageConverterTests { for (int i = 0; i < 2; i++) { bytes[i] = resultBytes.get(i).byteValue(); } - assertArrayEquals(new byte[]{0x1, 0x2}, bytes); + assertArrayEquals(new byte[] {0x1, 0x2}, bytes); } @Test @@ -114,9 +116,9 @@ public class GsonHttpMessageConverterTests { body.setString("Foo"); body.setNumber(42); body.setFraction(42F); - body.setArray(new String[]{"Foo", "Bar"}); + body.setArray(new String[] {"Foo", "Bar"}); body.setBool(true); - body.setBytes(new byte[]{0x1, 0x2}); + body.setBytes(new byte[] {0x1, 0x2}); this.converter.write(body, null, outputMessage); String result = outputMessage.getBodyAsString(UTF8); assertTrue(result.contains("\"string\":\"Foo\"")); @@ -129,6 +131,28 @@ public class GsonHttpMessageConverterTests { outputMessage.getHeaders().getContentType()); } + @Test + public void writeWithBaseType() throws IOException { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + MyBean body = new MyBean(); + body.setString("Foo"); + body.setNumber(42); + body.setFraction(42F); + body.setArray(new String[] {"Foo", "Bar"}); + body.setBool(true); + body.setBytes(new byte[] {0x1, 0x2}); + this.converter.write(body, MyBase.class, null, outputMessage); + String result = outputMessage.getBodyAsString(UTF8); + assertTrue(result.contains("\"string\":\"Foo\"")); + assertTrue(result.contains("\"number\":42")); + assertTrue(result.contains("fraction\":42.0")); + assertTrue(result.contains("\"array\":[\"Foo\",\"Bar\"]")); + assertTrue(result.contains("\"bool\":true")); + assertTrue(result.contains("\"bytes\":[1,2]")); + assertEquals("Invalid content-type", new MediaType("application", "json", UTF8), + outputMessage.getHeaders().getContentType()); + } + @Test public void writeUTF16() throws IOException { Charset utf16 = Charset.forName("UTF-16BE"); @@ -174,14 +198,14 @@ public class GsonHttpMessageConverterTests { assertEquals("Foo", result.getString()); assertEquals(42, result.getNumber()); assertEquals(42F, result.getFraction(), 0F); - assertArrayEquals(new String[] { "Foo", "Bar" }, result.getArray()); + assertArrayEquals(new String[] {"Foo", "Bar"}, result.getArray()); assertTrue(result.isBool()); - assertArrayEquals(new byte[] { 0x1, 0x2 }, result.getBytes()); + assertArrayEquals(new byte[] {0x1, 0x2}, result.getBytes()); } @Test @SuppressWarnings("unchecked") - public void readParameterizedType() throws IOException { + public void readAndWriteParameterizedType() throws Exception { ParameterizedTypeReference> beansList = new ParameterizedTypeReference>() { }; @@ -190,20 +214,49 @@ public class GsonHttpMessageConverterTests { MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(UTF8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); - GsonHttpMessageConverter converter = new GsonHttpMessageConverter(); List results = (List) converter.read(beansList.getType(), null, inputMessage); assertEquals(1, results.size()); MyBean result = results.get(0); assertEquals("Foo", result.getString()); assertEquals(42, result.getNumber()); assertEquals(42F, result.getFraction(), 0F); - assertArrayEquals(new String[] { "Foo", "Bar" }, result.getArray()); + assertArrayEquals(new String[] {"Foo", "Bar"}, result.getArray()); assertTrue(result.isBool()); - assertArrayEquals(new byte[] { 0x1, 0x2 }, result.getBytes()); + assertArrayEquals(new byte[] {0x1, 0x2}, result.getBytes()); + + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + converter.write(results, beansList.getType(), new MediaType("application", "json"), outputMessage); + JSONAssert.assertEquals(body, outputMessage.getBodyAsString(UTF8), true); } @Test - public void prefixJson() throws Exception { + @SuppressWarnings("unchecked") + public void writeParameterizedBaseType() throws Exception { + ParameterizedTypeReference> beansList = new ParameterizedTypeReference>() {}; + ParameterizedTypeReference> baseList = new ParameterizedTypeReference>() {}; + + String body = "[{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}]"; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(UTF8)); + inputMessage.getHeaders().setContentType(new MediaType("application", "json")); + + List results = (List) converter.read(beansList.getType(), null, inputMessage); + assertEquals(1, results.size()); + MyBean result = results.get(0); + assertEquals("Foo", result.getString()); + assertEquals(42, result.getNumber()); + assertEquals(42F, result.getFraction(), 0F); + assertArrayEquals(new String[] {"Foo", "Bar"}, result.getArray()); + assertTrue(result.isBool()); + assertArrayEquals(new byte[] {0x1, 0x2}, result.getBytes()); + + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + converter.write(results, baseList.getType(), new MediaType("application", "json"), outputMessage); + JSONAssert.assertEquals(body, outputMessage.getBodyAsString(UTF8), true); + } + + @Test + public void prefixJson() throws IOException { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); this.converter.setPrefixJson(true); this.converter.writeInternal("foo", null, outputMessage); @@ -211,7 +264,7 @@ public class GsonHttpMessageConverterTests { } @Test - public void prefixJsonCustom() throws Exception { + public void prefixJsonCustom() throws IOException { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); this.converter.setJsonPrefix(")))"); this.converter.writeInternal("foo", null, outputMessage); @@ -219,10 +272,22 @@ public class GsonHttpMessageConverterTests { } - public static class MyBean { + public static class MyBase { private String string; + public String getString() { + return string; + } + + public void setString(String string) { + this.string = string; + } + } + + + public static class MyBean extends MyBase { + private int number; private float fraction; @@ -233,30 +298,6 @@ public class GsonHttpMessageConverterTests { private byte[] bytes; - public byte[] getBytes() { - return bytes; - } - - public void setBytes(byte[] bytes) { - this.bytes = bytes; - } - - public boolean isBool() { - return bool; - } - - public void setBool(boolean bool) { - this.bool = bool; - } - - public String getString() { - return string; - } - - public void setString(String string) { - this.string = string; - } - public int getNumber() { return number; } @@ -280,6 +321,32 @@ public class GsonHttpMessageConverterTests { public void setArray(String[] array) { this.array = array; } + + public boolean isBool() { + return bool; + } + + public void setBool(boolean bool) { + this.bool = bool; + } + + public byte[] getBytes() { + return bytes; + } + + public void setBytes(byte[] bytes) { + this.bytes = bytes; + } + } + + + public static class ListHolder { + + public List listField; + } + + + public static class MyBeanListHolder extends ListHolder { } } diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java index 675cc0b844..641595334d 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ import com.fasterxml.jackson.databind.ser.FilterProvider; import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter; import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; import org.junit.Test; +import org.skyscreamer.jsonassert.JSONAssert; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; @@ -47,6 +48,7 @@ import static org.junit.Assert.*; * * @author Rossen Stoyanchev * @author Sebastien Deleuze + * @author Juergen Hoeller */ public class MappingJackson2HttpMessageConverterTests { @@ -75,24 +77,34 @@ public class MappingJackson2HttpMessageConverterTests { @Test public void readTyped() throws IOException { - String body = - "{\"bytes\":\"AQI=\",\"array\":[\"Foo\",\"Bar\"],\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; + String body = "{" + + "\"bytes\":\"AQI=\"," + + "\"array\":[\"Foo\",\"Bar\"]," + + "\"number\":42," + + "\"string\":\"Foo\"," + + "\"bool\":true," + + "\"fraction\":42.0}"; MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); MyBean result = (MyBean) converter.read(MyBean.class, inputMessage); assertEquals("Foo", result.getString()); assertEquals(42, result.getNumber()); assertEquals(42F, result.getFraction(), 0F); - assertArrayEquals(new String[]{"Foo", "Bar"}, result.getArray()); + assertArrayEquals(new String[] {"Foo", "Bar"}, result.getArray()); assertTrue(result.isBool()); - assertArrayEquals(new byte[]{0x1, 0x2}, result.getBytes()); + assertArrayEquals(new byte[] {0x1, 0x2}, result.getBytes()); } @Test @SuppressWarnings("unchecked") public void readUntyped() throws IOException { - String body = - "{\"bytes\":\"AQI=\",\"array\":[\"Foo\",\"Bar\"],\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; + String body = "{" + + "\"bytes\":\"AQI=\"," + + "\"array\":[\"Foo\",\"Bar\"]," + + "\"number\":42," + + "\"string\":\"Foo\"," + + "\"bool\":true," + + "\"fraction\":42.0}"; MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); HashMap result = (HashMap) converter.read(HashMap.class, inputMessage); @@ -114,9 +126,9 @@ public class MappingJackson2HttpMessageConverterTests { body.setString("Foo"); body.setNumber(42); body.setFraction(42F); - body.setArray(new String[]{"Foo", "Bar"}); + body.setArray(new String[] {"Foo", "Bar"}); body.setBool(true); - body.setBytes(new byte[]{0x1, 0x2}); + body.setBytes(new byte[] {0x1, 0x2}); converter.write(body, null, outputMessage); Charset utf8 = Charset.forName("UTF-8"); String result = outputMessage.getBodyAsString(utf8); @@ -130,6 +142,29 @@ public class MappingJackson2HttpMessageConverterTests { outputMessage.getHeaders().getContentType()); } + @Test + public void writeWithBaseType() throws IOException { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + MyBean body = new MyBean(); + body.setString("Foo"); + body.setNumber(42); + body.setFraction(42F); + body.setArray(new String[] {"Foo", "Bar"}); + body.setBool(true); + body.setBytes(new byte[] {0x1, 0x2}); + converter.write(body, MyBase.class, null, outputMessage); + Charset utf8 = Charset.forName("UTF-8"); + String result = outputMessage.getBodyAsString(utf8); + assertTrue(result.contains("\"string\":\"Foo\"")); + assertTrue(result.contains("\"number\":42")); + assertTrue(result.contains("fraction\":42.0")); + assertTrue(result.contains("\"array\":[\"Foo\",\"Bar\"]")); + assertTrue(result.contains("\"bool\":true")); + assertTrue(result.contains("\"bytes\":\"AQI=\"")); + assertEquals("Invalid content-type", new MediaType("application", "json", utf8), + outputMessage.getHeaders().getContentType()); + } + @Test public void writeUTF16() throws IOException { Charset utf16 = Charset.forName("UTF-16BE"); @@ -160,7 +195,7 @@ public class MappingJackson2HttpMessageConverterTests { @Test @SuppressWarnings("unchecked") - public void readGenerics() throws IOException { + public void readAndWriteGenerics() throws Exception { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter() { @Override protected JavaType getJavaType(Type type, Class contextClass) { @@ -172,8 +207,13 @@ public class MappingJackson2HttpMessageConverterTests { } } }; - String body = - "[{\"bytes\":\"AQI=\",\"array\":[\"Foo\",\"Bar\"],\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}]"; + String body = "[{" + + "\"bytes\":\"AQI=\"," + + "\"array\":[\"Foo\",\"Bar\"]," + + "\"number\":42," + + "\"string\":\"Foo\"," + + "\"bool\":true," + + "\"fraction\":42.0}]"; MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); @@ -183,18 +223,27 @@ public class MappingJackson2HttpMessageConverterTests { assertEquals("Foo", result.getString()); assertEquals(42, result.getNumber()); assertEquals(42F, result.getFraction(), 0F); - assertArrayEquals(new String[]{"Foo", "Bar"}, result.getArray()); + assertArrayEquals(new String[] {"Foo", "Bar"}, result.getArray()); assertTrue(result.isBool()); - assertArrayEquals(new byte[]{0x1, 0x2}, result.getBytes()); + assertArrayEquals(new byte[] {0x1, 0x2}, result.getBytes()); + + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + converter.write(results, new MediaType("application", "json"), outputMessage); + JSONAssert.assertEquals(body, outputMessage.getBodyAsString(Charset.forName("UTF-8")), true); } @Test @SuppressWarnings("unchecked") - public void readParameterizedType() throws IOException { + public void readAndWriteParameterizedType() throws Exception { ParameterizedTypeReference> beansList = new ParameterizedTypeReference>() {}; - String body = - "[{\"bytes\":\"AQI=\",\"array\":[\"Foo\",\"Bar\"],\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}]"; + String body = "[{" + + "\"bytes\":\"AQI=\"," + + "\"array\":[\"Foo\",\"Bar\"]," + + "\"number\":42," + + "\"string\":\"Foo\"," + + "\"bool\":true," + + "\"fraction\":42.0}]"; MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); @@ -205,11 +254,46 @@ public class MappingJackson2HttpMessageConverterTests { assertEquals("Foo", result.getString()); assertEquals(42, result.getNumber()); assertEquals(42F, result.getFraction(), 0F); - assertArrayEquals(new String[]{"Foo", "Bar"}, result.getArray()); + assertArrayEquals(new String[] {"Foo", "Bar"}, result.getArray()); assertTrue(result.isBool()); - assertArrayEquals(new byte[]{0x1, 0x2}, result.getBytes()); + assertArrayEquals(new byte[] {0x1, 0x2}, result.getBytes()); + + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + converter.write(results, beansList.getType(), new MediaType("application", "json"), outputMessage); + JSONAssert.assertEquals(body, outputMessage.getBodyAsString(Charset.forName("UTF-8")), true); } + @Test + @SuppressWarnings("unchecked") + public void writeParameterizedBaseType() throws Exception { + ParameterizedTypeReference> beansList = new ParameterizedTypeReference>() {}; + ParameterizedTypeReference> baseList = new ParameterizedTypeReference>() {}; + + String body = "[{" + + "\"bytes\":\"AQI=\"," + + "\"array\":[\"Foo\",\"Bar\"]," + + "\"number\":42," + + "\"string\":\"Foo\"," + + "\"bool\":true," + + "\"fraction\":42.0}]"; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + inputMessage.getHeaders().setContentType(new MediaType("application", "json")); + + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + List results = (List) converter.read(beansList.getType(), null, inputMessage); + assertEquals(1, results.size()); + MyBean result = results.get(0); + assertEquals("Foo", result.getString()); + assertEquals(42, result.getNumber()); + assertEquals(42F, result.getFraction(), 0F); + assertArrayEquals(new String[] {"Foo", "Bar"}, result.getArray()); + assertTrue(result.isBool()); + assertArrayEquals(new byte[] {0x1, 0x2}, result.getBytes()); + + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + converter.write(results, baseList.getType(), new MediaType("application", "json"), outputMessage); + JSONAssert.assertEquals(body, outputMessage.getBodyAsString(Charset.forName("UTF-8")), true); + } @Test public void prettyPrint() throws Exception { @@ -221,13 +305,14 @@ public class MappingJackson2HttpMessageConverterTests { this.converter.writeInternal(bean, null, outputMessage); String result = outputMessage.getBodyAsString(Charset.forName("UTF-8")); - assertEquals("{" + NEWLINE_SYSTEM_PROPERTY + " \"name\" : \"Jason\"" + NEWLINE_SYSTEM_PROPERTY + "}", result); + assertEquals("{" + NEWLINE_SYSTEM_PROPERTY + + " \"name\" : \"Jason\"" + NEWLINE_SYSTEM_PROPERTY + "}", result); } @Test public void prettyPrintWithSse() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - outputMessage.getHeaders().setContentType(new MediaType("text", "event-stream")); + outputMessage.getHeaders().setContentType(MediaType.TEXT_EVENT_STREAM); PrettyPrintBean bean = new PrettyPrintBean(); bean.setName("Jason"); @@ -342,7 +427,7 @@ public class MappingJackson2HttpMessageConverterTests { @Test // SPR-13318 public void writeSubTypeList() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - List beans = new ArrayList(); + List beans = new ArrayList<>(); MyBean foo = new MyBean(); foo.setString("Foo"); foo.setNumber(42); @@ -372,10 +457,22 @@ public class MappingJackson2HttpMessageConverterTests { } - public static class MyBean implements MyInterface { + public static class MyBase implements MyInterface{ private String string; + public String getString() { + return string; + } + + public void setString(String string) { + this.string = string; + } + } + + + public static class MyBean extends MyBase { + private int number; private float fraction; @@ -386,30 +483,6 @@ public class MappingJackson2HttpMessageConverterTests { private byte[] bytes; - public byte[] getBytes() { - return bytes; - } - - public void setBytes(byte[] bytes) { - this.bytes = bytes; - } - - public boolean isBool() { - return bool; - } - - public void setBool(boolean bool) { - this.bool = bool; - } - - public String getString() { - return string; - } - - public void setString(String string) { - this.string = string; - } - public int getNumber() { return number; } @@ -433,6 +506,22 @@ public class MappingJackson2HttpMessageConverterTests { public void setArray(String[] array) { this.array = array; } + + public boolean isBool() { + return bool; + } + + public void setBool(boolean bool) { + this.bool = bool; + } + + public byte[] getBytes() { + return bytes; + } + + public void setBytes(byte[] bytes) { + this.bytes = bytes; + } }