Introduce Jackson 3 support for converters

This commit introduces Jackson 3 SmartHttpMessageConverter based
variants of the following Jackson 2 classes (and related dependent classes).

org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter ->
org.springframework.http.converter.AbstractJacksonHttpMessageConverter

MappingJackson2HttpMessageConverter -> JacksonJsonHttpMessageConverter
MappingJackson2SmileHttpMessageConverter -> JacksonSmileHttpMessageConverter
MappingJackson2CborHttpMessageConverter -> JacksonCborHttpMessageConverter
MappingJackson2XmlHttpMessageConverter -> JacksonXmlHttpMessageConverter
MappingJackson2YamlHttpMessageConverter -> JacksonYamlHttpMessageConverter

They use hints instead of MappingJacksonValue and MappingJacksonInputMessage
to support `@JsonView` and FilterProvider.

Jackson 3 support is configured if found in the classpath otherwise
fallback to Jackson 2.

JacksonHandlerInstantiator needs to be enabled explicitly if needed.

See gh-33798
This commit is contained in:
Sébastien Deleuze
2025-05-13 12:38:18 +02:00
parent 7a8db31dc3
commit d4e4a9ae06
23 changed files with 3028 additions and 86 deletions

View File

@@ -0,0 +1,157 @@
/*
* Copyright 2002-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.converter.cbor;
import java.io.IOException;
import org.junit.jupiter.api.Test;
import tools.jackson.dataformat.cbor.CBORMapper;
import org.springframework.http.MediaType;
import org.springframework.web.testfixture.http.MockHttpInputMessage;
import org.springframework.web.testfixture.http.MockHttpOutputMessage;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.within;
/**
* Jackson 3.x CBOR converter tests.
*
* @author Sebastien Deleuze
*/
class JacksonCborHttpMessageConverterTests {
private final JacksonCborHttpMessageConverter converter = new JacksonCborHttpMessageConverter();
private final CBORMapper mapper = CBORMapper.builder().build();
@Test
void canRead() {
assertThat(converter.canRead(MyBean.class, MediaType.APPLICATION_CBOR)).isTrue();
assertThat(converter.canRead(MyBean.class, new MediaType("application", "json"))).isFalse();
assertThat(converter.canRead(MyBean.class, new MediaType("application", "xml"))).isFalse();
}
@Test
void canWrite() {
assertThat(converter.canWrite(MyBean.class, MediaType.APPLICATION_CBOR)).isTrue();
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "json"))).isFalse();
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "xml"))).isFalse();
}
@Test
void read() throws IOException {
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});
MockHttpInputMessage inputMessage = new MockHttpInputMessage(mapper.writeValueAsBytes(body));
inputMessage.getHeaders().setContentType(MediaType.APPLICATION_CBOR);
MyBean result = (MyBean) converter.read(MyBean.class, inputMessage);
assertThat(result.getString()).isEqualTo("Foo");
assertThat(result.getNumber()).isEqualTo(42);
assertThat(result.getFraction()).isCloseTo(42F, within(0F));
assertThat(result.getArray()).isEqualTo(new String[]{"Foo", "Bar"});
assertThat(result.isBool()).isTrue();
assertThat(result.getBytes()).isEqualTo(new byte[]{0x1, 0x2});
}
@Test
void write() 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, null, outputMessage);
assertThat(outputMessage.getBodyAsBytes()).isEqualTo(mapper.writeValueAsBytes(body));
assertThat(outputMessage.getHeaders().getContentType())
.as("Invalid content-type").isEqualTo(MediaType.APPLICATION_CBOR);
}
public static class MyBean {
private String string;
private int number;
private float fraction;
private String[] array;
private boolean bool;
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;
}
public void setNumber(int number) {
this.number = number;
}
public float getFraction() {
return fraction;
}
public void setFraction(float fraction) {
this.fraction = fraction;
}
public String[] getArray() {
return array;
}
public void setArray(String[] array) {
this.array = array;
}
}
}

View File

@@ -0,0 +1,843 @@
/*
* Copyright 2002-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.converter.json;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonView;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.skyscreamer.jsonassert.JSONAssert;
import tools.jackson.databind.JavaType;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectReader;
import tools.jackson.databind.ObjectWriter;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.cfg.EnumFeature;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.ser.FilterProvider;
import tools.jackson.databind.ser.std.SimpleBeanPropertyFilter;
import tools.jackson.databind.ser.std.SimpleFilterProvider;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.testfixture.http.MockHttpInputMessage;
import org.springframework.web.testfixture.http.MockHttpOutputMessage;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.entry;
import static org.assertj.core.api.Assertions.within;
/**
* Jackson 3.x converter tests.
*
* @author Sebastien Deleuze
*/
class JacksonJsonHttpMessageConverterTests {
private JacksonJsonHttpMessageConverter converter = new JacksonJsonHttpMessageConverter();
@Test
void canRead() {
assertThat(converter.canRead(MyBean.class, new MediaType("application", "json"))).isTrue();
assertThat(converter.canRead(Map.class, new MediaType("application", "json"))).isTrue();
assertThat(converter.canRead(MyBean.class, new MediaType("application", "json", StandardCharsets.UTF_8))).isTrue();
assertThat(converter.canRead(MyBean.class, new MediaType("application", "json", StandardCharsets.US_ASCII))).isTrue();
assertThat(converter.canRead(MyBean.class, new MediaType("application", "json", StandardCharsets.ISO_8859_1))).isTrue();
}
@Test
void canReadWithObjectMapperRegistrationForType() {
MediaType halJsonMediaType = MediaType.parseMediaType("application/hal+json");
MediaType halFormsJsonMediaType = MediaType.parseMediaType("application/prs.hal-forms+json");
assertThat(converter.canRead(MyBean.class, halJsonMediaType)).isTrue();
assertThat(converter.canRead(MyBean.class, MediaType.APPLICATION_JSON)).isTrue();
assertThat(converter.canRead(MyBean.class, halFormsJsonMediaType)).isTrue();
assertThat(converter.canRead(Map.class, MediaType.APPLICATION_JSON)).isTrue();
converter.registerObjectMappersForType(MyBean.class, map -> {
map.put(halJsonMediaType, new ObjectMapper());
map.put(MediaType.APPLICATION_JSON, new ObjectMapper());
});
assertThat(converter.canRead(MyBean.class, halJsonMediaType)).isTrue();
assertThat(converter.canRead(MyBean.class, MediaType.APPLICATION_JSON)).isTrue();
assertThat(converter.canRead(Map.class, MediaType.APPLICATION_JSON)).isTrue();
}
@Test
void canWrite() {
assertThat(converter.canWrite(MyBean.class, MediaType.APPLICATION_JSON)).isTrue();
assertThat(converter.canWrite(Map.class, MediaType.APPLICATION_JSON)).isTrue();
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "json", StandardCharsets.UTF_8))).isTrue();
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "json", StandardCharsets.US_ASCII))).isTrue();
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "json", StandardCharsets.ISO_8859_1))).isFalse();
assertThatThrownBy(() -> converter.canWrite(MappingJacksonValue.class, MediaType.APPLICATION_JSON)).isInstanceOf(UnsupportedOperationException.class);
}
@Test // SPR-7905
void canReadAndWriteMicroformats() {
assertThat(converter.canRead(MyBean.class, new MediaType("application", "vnd.test-micro-type+json"))).isTrue();
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "vnd.test-micro-type+json"))).isTrue();
}
@Test
void getSupportedMediaTypes() {
MediaType[] defaultMediaTypes = {MediaType.APPLICATION_JSON, MediaType.parseMediaType("application/*+json")};
assertThat(converter.getSupportedMediaTypes()).containsExactly(defaultMediaTypes);
assertThat(converter.getSupportedMediaTypes(MyBean.class)).containsExactly(defaultMediaTypes);
MediaType halJson = MediaType.parseMediaType("application/hal+json");
converter.registerObjectMappersForType(MyBean.class, map -> {
map.put(halJson, new ObjectMapper());
map.put(MediaType.APPLICATION_JSON, new ObjectMapper());
});
assertThat(converter.getSupportedMediaTypes(MyBean.class)).containsExactly(halJson, MediaType.APPLICATION_JSON);
assertThat(converter.getSupportedMediaTypes(Map.class)).containsExactly(defaultMediaTypes);
}
@Test
void readTyped() throws IOException {
String body = "{" +
"\"bytes\":\"AQI=\"," +
"\"array\":[\"Foo\",\"Bar\"]," +
"\"number\":42," +
"\"string\":\"Foo\"," +
"\"bool\":true," +
"\"fraction\":42.0}";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8));
inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
MyBean result = (MyBean) converter.read(MyBean.class, inputMessage);
assertThat(result.getString()).isEqualTo("Foo");
assertThat(result.getNumber()).isEqualTo(42);
assertThat(result.getFraction()).isCloseTo(42F, within(0F));
assertThat(result.getArray()).isEqualTo(new String[] {"Foo", "Bar"});
assertThat(result.isBool()).isTrue();
assertThat(result.getBytes()).isEqualTo(new byte[] {0x1, 0x2});
}
@Test
@SuppressWarnings("unchecked")
void readUntyped() throws IOException {
String body = "{" +
"\"bytes\":\"AQI=\"," +
"\"array\":[\"Foo\",\"Bar\"]," +
"\"number\":42," +
"\"string\":\"Foo\"," +
"\"bool\":true," +
"\"fraction\":42.0}";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8));
inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
HashMap<String, Object> result = (HashMap<String, Object>) converter.read(HashMap.class, inputMessage);
assertThat(result).containsEntry("string", "Foo");
assertThat(result).containsEntry("number", 42);
assertThat((Double) result.get("fraction")).isCloseTo(42D, within(0D));
List<String> array = new ArrayList<>();
array.add("Foo");
array.add("Bar");
assertThat(result).containsEntry("array", array);
assertThat(result).containsEntry("bool", Boolean.TRUE);
assertThat(result).containsEntry("bytes", "AQI=");
}
@Test
void write() 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, null, outputMessage);
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8);
assertThat(result).contains("\"string\":\"Foo\"");
assertThat(result).contains("\"number\":42");
assertThat(result).contains("fraction\":42.0");
assertThat(result).contains("\"array\":[\"Foo\",\"Bar\"]");
assertThat(result).contains("\"bool\":true");
assertThat(result).contains("\"bytes\":\"AQI=\"");
assertThat(outputMessage.getHeaders().getContentType())
.as("Invalid content-type").isEqualTo(MediaType.APPLICATION_JSON);
}
@Test
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, ResolvableType.forClass(MyBase.class), null, outputMessage, null);
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8);
assertThat(result).contains("\"string\":\"Foo\"");
assertThat(result).contains("\"number\":42");
assertThat(result).contains("fraction\":42.0");
assertThat(result).contains("\"array\":[\"Foo\",\"Bar\"]");
assertThat(result).contains("\"bool\":true");
assertThat(result).contains("\"bytes\":\"AQI=\"");
assertThat(outputMessage.getHeaders().getContentType())
.as("Invalid content-type").isEqualTo(MediaType.APPLICATION_JSON);
}
@Test
void writeUTF16() throws IOException {
MediaType contentType = new MediaType("application", "json", StandardCharsets.UTF_16BE);
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
String body = "H\u00e9llo W\u00f6rld";
converter.write(body, contentType, outputMessage);
assertThat(outputMessage.getBodyAsString(StandardCharsets.UTF_16BE)).as("Invalid result").isEqualTo(("\"" + body + "\""));
assertThat(outputMessage.getHeaders().getContentType()).as("Invalid content-type").isEqualTo(contentType);
}
@Test
void readInvalidJson() {
String body = "FooBar";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8));
inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
assertThatExceptionOfType(HttpMessageNotReadableException.class)
.isThrownBy(() -> converter.read(MyBean.class, inputMessage));
}
@Test
void readValidJsonWithUnknownProperty() throws IOException {
String body = "{\"string\":\"string\",\"unknownProperty\":\"value\"}";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8));
inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
converter.read(MyBean.class, inputMessage);
// Assert no HttpMessageNotReadableException is thrown
}
@Test
@SuppressWarnings("unchecked")
void readAndWriteGenerics() throws Exception {
JacksonJsonHttpMessageConverter converter = new JacksonJsonHttpMessageConverter() {
@Override
protected JavaType getJavaType(Type type, @Nullable Class<?> contextClass) {
if (type instanceof Class && List.class.isAssignableFrom((Class<?>)type)) {
return new ObjectMapper().getTypeFactory().constructCollectionType(ArrayList.class, MyBean.class);
}
else {
return super.getJavaType(type, contextClass);
}
}
};
String body = "[{" +
"\"bytes\":\"AQI=\"," +
"\"array\":[\"Foo\",\"Bar\"]," +
"\"number\":42," +
"\"string\":\"Foo\"," +
"\"bool\":true," +
"\"fraction\":42.0}]";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8));
inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
List<MyBean> results = (List<MyBean>) converter.read(List.class, inputMessage);
assertThat(results).hasSize(1);
MyBean result = results.get(0);
assertThat(result.getString()).isEqualTo("Foo");
assertThat(result.getNumber()).isEqualTo(42);
assertThat(result.getFraction()).isCloseTo(42F, within(0F));
assertThat(result.getArray()).isEqualTo(new String[] {"Foo", "Bar"});
assertThat(result.isBool()).isTrue();
assertThat(result.getBytes()).isEqualTo(new byte[] {0x1, 0x2});
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
converter.write(results, MediaType.APPLICATION_JSON, outputMessage);
JSONAssert.assertEquals(body, outputMessage.getBodyAsString(StandardCharsets.UTF_8), true);
}
@Test
@SuppressWarnings("unchecked")
void readAndWriteParameterizedType() throws Exception {
ParameterizedTypeReference<List<MyBean>> beansList = new ParameterizedTypeReference<>() {};
String body = "[{" +
"\"bytes\":\"AQI=\"," +
"\"array\":[\"Foo\",\"Bar\"]," +
"\"number\":42," +
"\"string\":\"Foo\"," +
"\"bool\":true," +
"\"fraction\":42.0}]";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8));
inputMessage.getHeaders().setContentType(MediaType.APPLICATION_JSON);
JacksonJsonHttpMessageConverter converter = new JacksonJsonHttpMessageConverter();
List<MyBean> results = (List<MyBean>) converter.read(ResolvableType.forType(beansList), inputMessage, null);
assertThat(results).hasSize(1);
MyBean result = results.get(0);
assertThat(result.getString()).isEqualTo("Foo");
assertThat(result.getNumber()).isEqualTo(42);
assertThat(result.getFraction()).isCloseTo(42F, within(0F));
assertThat(result.getArray()).isEqualTo(new String[] {"Foo", "Bar"});
assertThat(result.isBool()).isTrue();
assertThat(result.getBytes()).isEqualTo(new byte[] {0x1, 0x2});
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
converter.write(results, ResolvableType.forType(beansList), MediaType.APPLICATION_JSON, outputMessage, null);
JSONAssert.assertEquals(body, outputMessage.getBodyAsString(StandardCharsets.UTF_8), true);
}
@Test
@SuppressWarnings("unchecked")
void writeParameterizedBaseType() throws Exception {
ParameterizedTypeReference<List<MyBean>> beansList = new ParameterizedTypeReference<>() {};
ParameterizedTypeReference<List<MyBase>> 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(StandardCharsets.UTF_8));
inputMessage.getHeaders().setContentType(MediaType.APPLICATION_JSON);
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
List<MyBean> results = (List<MyBean>) converter.read(beansList.getType(), null, inputMessage);
assertThat(results).hasSize(1);
MyBean result = results.get(0);
assertThat(result.getString()).isEqualTo("Foo");
assertThat(result.getNumber()).isEqualTo(42);
assertThat(result.getFraction()).isCloseTo(42F, within(0F));
assertThat(result.getArray()).isEqualTo(new String[] {"Foo", "Bar"});
assertThat(result.isBool()).isTrue();
assertThat(result.getBytes()).isEqualTo(new byte[] {0x1, 0x2});
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
converter.write(results, baseList.getType(), MediaType.APPLICATION_JSON, outputMessage);
JSONAssert.assertEquals(body, outputMessage.getBodyAsString(StandardCharsets.UTF_8), true);
}
// gh-24498
@Test
void writeOptional() throws IOException {
ParameterizedTypeReference<Optional<MyParent>> optionalParent = new ParameterizedTypeReference<>() {};
Optional<MyParent> result = Optional.of(new Impl1());
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
converter.write(result, ResolvableType.forType(optionalParent.getType()),
MediaType.APPLICATION_JSON, outputMessage, null);
assertThat(outputMessage.getBodyAsString(StandardCharsets.UTF_8))
.contains("@type");
}
@Test
void prettyPrint() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
PrettyPrintBean bean = new PrettyPrintBean();
bean.setName("Jason");
ObjectMapper mapper = JsonMapper.builder().enable(SerializationFeature.INDENT_OUTPUT).build();
this.converter = new JacksonJsonHttpMessageConverter(mapper);
this.converter.write(bean, ResolvableType.forType(PrettyPrintBean.class),
MediaType.APPLICATION_JSON, outputMessage, null);
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8);
assertThat(result).isEqualToNormalizingNewlines("""
{
\s "name" : "Jason"
}""");
}
@Test
void prettyPrintWithSse() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
outputMessage.getHeaders().setContentType(MediaType.TEXT_EVENT_STREAM);
PrettyPrintBean bean = new PrettyPrintBean();
bean.setName("Jason");
ObjectMapper mapper = JsonMapper.builder().enable(SerializationFeature.INDENT_OUTPUT).build();
this.converter = new JacksonJsonHttpMessageConverter(mapper);
this.converter.write(bean, ResolvableType.forType(PrettyPrintBean.class),
MediaType.APPLICATION_JSON, outputMessage, null);
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8);
assertThat(result).isEqualTo("{\ndata: \"name\" : \"Jason\"\ndata:}");
}
@Test
void prefixJson() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
this.converter.setPrefixJson(true);
this.converter.write("foo", ResolvableType.forType(String.class), MediaType.APPLICATION_JSON,
outputMessage, null);
assertThat(outputMessage.getBodyAsString(StandardCharsets.UTF_8)).isEqualTo(")]}', \"foo\"");
}
@Test
void prefixJsonCustom() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
this.converter.setJsonPrefix(")))");
this.converter.write("foo", ResolvableType.forType(String.class), MediaType.APPLICATION_JSON,
outputMessage, null);
assertThat(outputMessage.getBodyAsString(StandardCharsets.UTF_8)).isEqualTo(")))\"foo\"");
}
@Test
void fieldLevelJsonView() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
JacksonViewBean bean = new JacksonViewBean();
bean.setWithView1("with");
bean.setWithView2("with");
bean.setWithoutView("without");
Map<String, Object> hints = Collections.singletonMap(JsonView.class.getName(), MyJacksonView1.class);
this.converter.write(bean, ResolvableType.forType(JacksonViewBean.class), MediaType.APPLICATION_JSON,
outputMessage, hints);
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8);
assertThat(result).contains("\"withView1\":\"with\"");
assertThat(result).doesNotContain("\"withView2\":\"with\"");
assertThat(result).doesNotContain("\"withoutView\":\"without\"");
}
@Test
void classLevelJsonView() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
JacksonViewBean bean = new JacksonViewBean();
bean.setWithView1("with");
bean.setWithView2("with");
bean.setWithoutView("without");
Map<String, Object> hints = Collections.singletonMap(JsonView.class.getName(), MyJacksonView3.class);
this.converter.write(bean, ResolvableType.forType(JacksonViewBean.class), MediaType.APPLICATION_JSON,
outputMessage, hints);
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8);
assertThat(result).doesNotContain("\"withView1\":\"with\"");
assertThat(result).doesNotContain("\"withView2\":\"with\"");
assertThat(result).contains("\"withoutView\":\"without\"");
}
@Test
void filters() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
JacksonFilteredBean bean = new JacksonFilteredBean();
bean.setProperty1("value");
bean.setProperty2("value");
FilterProvider filters = new SimpleFilterProvider().addFilter("myJacksonFilter",
SimpleBeanPropertyFilter.serializeAllExcept("property2"));
Map<String, Object> hints = Collections.singletonMap(FilterProvider.class.getName(), filters);
this.converter.write(bean, ResolvableType.forType(JacksonFilteredBean.class), MediaType.APPLICATION_JSON,
outputMessage, hints);
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8);
assertThat(result).contains("\"property1\":\"value\"");
assertThat(result).doesNotContain("\"property2\":\"value\"");
}
@Test // SPR-13318
void writeSubType() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
MyBean bean = new MyBean();
bean.setString("Foo");
bean.setNumber(42);
this.converter.write(bean, ResolvableType.forType(MyInterface.class),
MediaType.APPLICATION_JSON, outputMessage, null);
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8);
assertThat(result).contains("\"string\":\"Foo\"");
assertThat(result).contains("\"number\":42");
}
@Test // SPR-13318
void writeSubTypeList() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
List<MyBean> beans = new ArrayList<>();
MyBean foo = new MyBean();
foo.setString("Foo");
foo.setNumber(42);
beans.add(foo);
MyBean bar = new MyBean();
bar.setString("Bar");
bar.setNumber(123);
beans.add(bar);
ParameterizedTypeReference<List<MyInterface>> typeReference =
new ParameterizedTypeReference<>() {};
this.converter.write(beans, ResolvableType.forType(typeReference), MediaType.APPLICATION_JSON,
outputMessage, null);
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8);
assertThat(result).contains("\"string\":\"Foo\"");
assertThat(result).contains("\"number\":42");
assertThat(result).contains("\"string\":\"Bar\"");
assertThat(result).contains("\"number\":123");
}
@Test // gh-27511
void readWithNoDefaultConstructor() throws Exception {
String body = "{\"property1\":\"foo\",\"property2\":\"bar\"}";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8));
inputMessage.getHeaders().setContentType(MediaType.APPLICATION_JSON);
BeanWithNoDefaultConstructor bean =
(BeanWithNoDefaultConstructor)converter.read(BeanWithNoDefaultConstructor.class, inputMessage);
assertThat(bean.property1).isEqualTo("foo");
assertThat(bean.property2).isEqualTo("bar");
}
@Test
@SuppressWarnings("unchecked")
void readNonUnicode() throws Exception {
String body = "{\"føø\":\"bår\"}";
Charset charset = StandardCharsets.ISO_8859_1;
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(charset));
inputMessage.getHeaders().setContentType(new MediaType("application", "json", charset));
HashMap<String, Object> result = (HashMap<String, Object>) this.converter.read(HashMap.class, inputMessage);
assertThat(result).containsExactly(entry("føø", "bår"));
}
@Test
@SuppressWarnings("unchecked")
void readAscii() throws Exception {
String body = "{\"foo\":\"bar\"}";
Charset charset = StandardCharsets.US_ASCII;
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(charset));
inputMessage.getHeaders().setContentType(new MediaType("application", "json", charset));
HashMap<String, Object> result = (HashMap<String, Object>) this.converter.read(HashMap.class, inputMessage);
assertThat(result).containsExactly(entry("foo", "bar"));
}
@Test
void writeAscii() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
Map<String,Object> body = new HashMap<>();
body.put("foo", "bar");
Charset charset = StandardCharsets.US_ASCII;
MediaType contentType = new MediaType("application", "json", charset);
converter.write(body, contentType, outputMessage);
String result = outputMessage.getBodyAsString(charset);
assertThat(result).isEqualTo("{\"foo\":\"bar\"}");
assertThat(outputMessage.getHeaders().getContentType()).as("Invalid content-type").isEqualTo(contentType);
}
@Test
void readWithCustomized() throws IOException {
JacksonJsonHttpMessageConverterWithCustomization customizedConverter =
new JacksonJsonHttpMessageConverterWithCustomization();
String body = "{\"property\":\"Value1\"}";
MockHttpInputMessage inputMessage1 = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8));
MockHttpInputMessage inputMessage2 = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8));
inputMessage1.getHeaders().setContentType(new MediaType("application", "json"));
inputMessage2.getHeaders().setContentType(new MediaType("application", "json"));
assertThatExceptionOfType(HttpMessageNotReadableException.class)
.isThrownBy(() -> customizedConverter.read(MyCustomizedBean.class, inputMessage1));
MyCustomizedBean customizedResult = (MyCustomizedBean) converter.read(MyCustomizedBean.class, inputMessage2);
assertThat(customizedResult.getProperty()).isEqualTo(MyCustomEnum.VAL1);
}
@Test
void writeWithCustomized() throws IOException {
JacksonJsonHttpMessageConverterWithCustomization customizedConverter =
new JacksonJsonHttpMessageConverterWithCustomization();
MockHttpOutputMessage outputMessage1 = new MockHttpOutputMessage();
MockHttpOutputMessage outputMessage2 = new MockHttpOutputMessage();
MyCustomizedBean body = new MyCustomizedBean();
body.setProperty(MyCustomEnum.VAL2);
converter.write(body, null, outputMessage1);
customizedConverter.write(body, null, outputMessage2);
String result1 = outputMessage1.getBodyAsString(StandardCharsets.UTF_8);
assertThat(result1).contains("\"property\":\"Value2\"");
String result2 = outputMessage2.getBodyAsString(StandardCharsets.UTF_8);
assertThat(result2).contains("\"property\":\"VAL2\"");
}
@Test
void repeatableWrites() throws IOException {
MockHttpOutputMessage outputMessage1 = new MockHttpOutputMessage();
MyBean body = new MyBean();
body.setString("Foo");
converter.write(body, null, outputMessage1);
String result = outputMessage1.getBodyAsString(StandardCharsets.UTF_8);
assertThat(result).contains("\"string\":\"Foo\"");
MockHttpOutputMessage outputMessage2 = new MockHttpOutputMessage();
converter.write(body, null, outputMessage2);
result = outputMessage2.getBodyAsString(StandardCharsets.UTF_8);
assertThat(result).contains("\"string\":\"Foo\"");
}
interface MyInterface {
String getString();
void setString(String string);
}
public static class MyBase implements MyInterface {
private String string;
@Override
public String getString() {
return string;
}
@Override
public void setString(String string) {
this.string = string;
}
}
public static class MyBean extends MyBase {
private int number;
private float fraction;
private String[] array;
private boolean bool;
private byte[] bytes;
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public float getFraction() {
return fraction;
}
public void setFraction(float fraction) {
this.fraction = fraction;
}
public String[] getArray() {
return array;
}
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 PrettyPrintBean {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
private interface MyJacksonView1 {}
private interface MyJacksonView2 {}
private interface MyJacksonView3 {}
@SuppressWarnings("unused")
@JsonView(MyJacksonView3.class)
private static class JacksonViewBean {
@JsonView(MyJacksonView1.class)
private String withView1;
@JsonView(MyJacksonView2.class)
private String withView2;
private String withoutView;
public String getWithView1() {
return withView1;
}
public void setWithView1(String withView1) {
this.withView1 = withView1;
}
public String getWithView2() {
return withView2;
}
public void setWithView2(String withView2) {
this.withView2 = withView2;
}
public String getWithoutView() {
return withoutView;
}
public void setWithoutView(String withoutView) {
this.withoutView = withoutView;
}
}
@JsonFilter("myJacksonFilter")
@SuppressWarnings("unused")
private static class JacksonFilteredBean {
private String property1;
private String property2;
public String getProperty1() {
return property1;
}
public void setProperty1(String property1) {
this.property1 = property1;
}
public String getProperty2() {
return property2;
}
public void setProperty2(String property2) {
this.property2 = property2;
}
}
@SuppressWarnings("unused")
private static class BeanWithNoDefaultConstructor {
private final String property1;
private final String property2;
public BeanWithNoDefaultConstructor(String property1, String property2) {
this.property1 = property1;
this.property2 = property2;
}
public String getProperty1() {
return property1;
}
public String getProperty2() {
return property2;
}
}
public static class MyCustomizedBean {
private MyCustomEnum property;
public MyCustomEnum getProperty() {
return property;
}
public void setProperty(MyCustomEnum property) {
this.property = property;
}
}
public enum MyCustomEnum {
VAL1,
VAL2;
@Override
public String toString() {
return this == VAL1 ? "Value1" : "Value2";
}
}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
@JsonSubTypes(value = {@JsonSubTypes.Type(value = Impl1.class),
@JsonSubTypes.Type(value = Impl2.class)})
public interface MyParent {
}
public static class Impl1 implements MyParent {
}
public static class Impl2 implements MyParent {
}
private static class JacksonJsonHttpMessageConverterWithCustomization extends JacksonJsonHttpMessageConverter {
@Override
protected ObjectReader customizeReader(ObjectReader reader, JavaType javaType) {
return reader.without(EnumFeature.READ_ENUMS_USING_TO_STRING);
}
@Override
protected ObjectWriter customizeWriter(ObjectWriter writer, @Nullable JavaType javaType, @Nullable MediaType contentType) {
return writer.without(EnumFeature.WRITE_ENUMS_USING_TO_STRING);
}
}
}

View File

@@ -0,0 +1,157 @@
/*
* Copyright 2002-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.converter.smile;
import java.io.IOException;
import org.junit.jupiter.api.Test;
import tools.jackson.dataformat.smile.SmileMapper;
import org.springframework.http.MediaType;
import org.springframework.web.testfixture.http.MockHttpInputMessage;
import org.springframework.web.testfixture.http.MockHttpOutputMessage;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.within;
/**
* Jackson 3.x Smile converter tests.
*
* @author Sebastien Deleuze
*/
class JacksonSmileHttpMessageConverterTests {
private final JacksonSmileHttpMessageConverter converter = new JacksonSmileHttpMessageConverter();
private final SmileMapper mapper = SmileMapper.builder().build();
@Test
void canRead() {
assertThat(converter.canRead(MyBean.class, new MediaType("application", "x-jackson-smile"))).isTrue();
assertThat(converter.canRead(MyBean.class, new MediaType("application", "json"))).isFalse();
assertThat(converter.canRead(MyBean.class, new MediaType("application", "xml"))).isFalse();
}
@Test
void canWrite() {
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "x-jackson-smile"))).isTrue();
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "json"))).isFalse();
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "xml"))).isFalse();
}
@Test
void read() throws IOException {
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});
MockHttpInputMessage inputMessage = new MockHttpInputMessage(mapper.writeValueAsBytes(body));
inputMessage.getHeaders().setContentType(new MediaType("application", "x-jackson-smile"));
MyBean result = (MyBean) converter.read(MyBean.class, inputMessage);
assertThat(result.getString()).isEqualTo("Foo");
assertThat(result.getNumber()).isEqualTo(42);
assertThat(result.getFraction()).isCloseTo(42F, within(0F));
assertThat(result.getArray()).isEqualTo(new String[]{"Foo", "Bar"});
assertThat(result.isBool()).isTrue();
assertThat(result.getBytes()).isEqualTo(new byte[]{0x1, 0x2});
}
@Test
void write() 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, null, outputMessage);
assertThat(outputMessage.getBodyAsBytes()).isEqualTo(mapper.writeValueAsBytes(body));
assertThat(outputMessage.getHeaders().getContentType())
.as("Invalid content-type").isEqualTo(new MediaType("application", "x-jackson-smile"));
}
public static class MyBean {
private String string;
private int number;
private float fraction;
private String[] array;
private boolean bool;
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;
}
public void setNumber(int number) {
this.number = number;
}
public float getFraction() {
return fraction;
}
public void setFraction(float fraction) {
this.fraction = fraction;
}
public String[] getArray() {
return array;
}
public void setArray(String[] array) {
this.array = array;
}
}
}

View File

@@ -0,0 +1,319 @@
/*
* Copyright 2002-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.converter.xml;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonView;
import org.junit.jupiter.api.Test;
import tools.jackson.dataformat.xml.XmlMapper;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.testfixture.http.MockHttpInputMessage;
import org.springframework.web.testfixture.http.MockHttpOutputMessage;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.within;
/**
* Jackson 3.x XML converter tests.
*
* @author Sebastien Deleuze
*/
class JacksonXmlHttpMessageConverterTests {
private final JacksonXmlHttpMessageConverter converter = new JacksonXmlHttpMessageConverter();
@Test
void canRead() {
assertThat(converter.canRead(MyBean.class, new MediaType("application", "xml"))).isTrue();
assertThat(converter.canRead(MyBean.class, new MediaType("text", "xml"))).isTrue();
assertThat(converter.canRead(MyBean.class, new MediaType("application", "soap+xml"))).isTrue();
assertThat(converter.canRead(MyBean.class, new MediaType("text", "xml", StandardCharsets.UTF_8))).isTrue();
assertThat(converter.canRead(MyBean.class, new MediaType("text", "xml", StandardCharsets.ISO_8859_1))).isTrue();
}
@Test
void canWrite() {
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "xml"))).isTrue();
assertThat(converter.canWrite(MyBean.class, new MediaType("text", "xml"))).isTrue();
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "soap+xml"))).isTrue();
assertThat(converter.canWrite(MyBean.class, new MediaType("text", "xml", StandardCharsets.UTF_8))).isTrue();
assertThat(converter.canWrite(MyBean.class, new MediaType("text", "xml", StandardCharsets.ISO_8859_1))).isFalse();
}
@Test
void read() throws IOException {
String body = "<MyBean>" +
"<string>Foo</string>" +
"<number>42</number>" +
"<fraction>42.0</fraction>" +
"<array><array>Foo</array>" +
"<array>Bar</array></array>" +
"<bool>true</bool>" +
"<bytes>AQI=</bytes></MyBean>";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8));
inputMessage.getHeaders().setContentType(new MediaType("application", "xml"));
MyBean result = (MyBean) converter.read(MyBean.class, inputMessage);
assertThat(result.getString()).isEqualTo("Foo");
assertThat(result.getNumber()).isEqualTo(42);
assertThat(result.getFraction()).isCloseTo(42F, within(0F));
assertThat(result.getArray()).isEqualTo(new String[]{"Foo", "Bar"});
assertThat(result.isBool()).isTrue();
assertThat(result.getBytes()).isEqualTo(new byte[]{0x1, 0x2});
}
@Test
void write() 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, null, outputMessage);
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8);
assertThat(result).contains("<string>Foo</string>");
assertThat(result).contains("<number>42</number>");
assertThat(result).contains("<fraction>42.0</fraction>");
assertThat(result).contains("<array><array>Foo</array><array>Bar</array></array>");
assertThat(result).contains("<bool>true</bool>");
assertThat(result).contains("<bytes>AQI=</bytes>");
assertThat(outputMessage.getHeaders().getContentType())
.as("Invalid content-type").isEqualTo(new MediaType("application", "xml", StandardCharsets.UTF_8));
}
@Test
void readInvalidXml() {
String body = "FooBar";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8));
inputMessage.getHeaders().setContentType(MediaType.APPLICATION_XML);
assertThatExceptionOfType(HttpMessageNotReadableException.class).isThrownBy(() ->
converter.read(MyBean.class, inputMessage));
}
@Test
void readValidXmlWithUnknownProperty() throws IOException {
String body = "<MyBean><string>string</string><unknownProperty>value</unknownProperty></MyBean>";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8));
inputMessage.getHeaders().setContentType(MediaType.APPLICATION_XML);
converter.read(MyBean.class, inputMessage);
// Assert no HttpMessageNotReadableException is thrown
}
@Test
void jsonView() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
JacksonViewBean bean = new JacksonViewBean();
bean.setWithView1("with");
bean.setWithView2("with");
bean.setWithoutView("without");
Map<String, Object> hints = Collections.singletonMap(JsonView.class.getName(), MyJacksonView1.class);
this.converter.write(bean, ResolvableType.forClass(JacksonViewBean.class), null, outputMessage, hints);
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8);
assertThat(result).contains("<withView1>with</withView1>");
assertThat(result).doesNotContain("<withView2>with</withView2>");
assertThat(result).doesNotContain("<withoutView>without</withoutView>");
}
@Test
void customXmlMapper() {
new JacksonXmlHttpMessageConverter(new MyXmlMapper());
// Assert no exception is thrown
}
@Test
void readWithExternalReference() throws IOException {
String body = "<!DOCTYPE MyBean SYSTEM \"https://192.168.28.42/1.jsp\" [" +
" <!ELEMENT root ANY >\n" +
" <!ENTITY ext SYSTEM \"" +
new ClassPathResource("external.txt", getClass()).getURI() +
"\" >]><MyBean><string>&ext;</string></MyBean>";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8));
inputMessage.getHeaders().setContentType(MediaType.APPLICATION_XML);
assertThatExceptionOfType(HttpMessageNotReadableException.class).isThrownBy(() ->
this.converter.read(MyBean.class, inputMessage));
}
@Test
void readWithXmlBomb() {
// https://en.wikipedia.org/wiki/Billion_laughs
// https://msdn.microsoft.com/en-us/magazine/ee335713.aspx
String body = """
<?xml version="1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ELEMENT lolz (#PCDATA)>
<!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<MyBean>&lol9;</MyBean>""";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8));
inputMessage.getHeaders().setContentType(MediaType.APPLICATION_XML);
assertThatExceptionOfType(HttpMessageNotReadableException.class).isThrownBy(() ->
this.converter.read(MyBean.class, inputMessage));
}
@Test
void readNonUnicode() throws Exception {
String body = "<MyBean>" +
"<string>føø bår</string>" +
"</MyBean>";
Charset charset = StandardCharsets.ISO_8859_1;
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(charset));
inputMessage.getHeaders().setContentType(new MediaType("application", "xml", charset));
MyBean result = (MyBean) converter.read(MyBean.class, inputMessage);
assertThat(result.getString()).isEqualTo("føø bår");
}
public static class MyBean {
private String string;
private int number;
private float fraction;
private String[] array;
private boolean bool;
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;
}
public void setNumber(int number) {
this.number = number;
}
public float getFraction() {
return fraction;
}
public void setFraction(float fraction) {
this.fraction = fraction;
}
public String[] getArray() {
return array;
}
public void setArray(String[] array) {
this.array = array;
}
}
private interface MyJacksonView1 {}
private interface MyJacksonView2 {}
@SuppressWarnings("unused")
private static class JacksonViewBean {
@JsonView(MyJacksonView1.class)
private String withView1;
@JsonView(MyJacksonView2.class)
private String withView2;
private String withoutView;
public String getWithView1() {
return withView1;
}
public void setWithView1(String withView1) {
this.withView1 = withView1;
}
public String getWithView2() {
return withView2;
}
public void setWithView2(String withView2) {
this.withView2 = withView2;
}
public String getWithoutView() {
return withoutView;
}
public void setWithoutView(String withoutView) {
this.withoutView = withoutView;
}
}
private static class MyXmlMapper extends XmlMapper {
}
}

View File

@@ -0,0 +1,157 @@
/*
* Copyright 2002-2025 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.converter.yaml;
import java.io.IOException;
import org.junit.jupiter.api.Test;
import tools.jackson.dataformat.yaml.YAMLMapper;
import org.springframework.http.MediaType;
import org.springframework.web.testfixture.http.MockHttpInputMessage;
import org.springframework.web.testfixture.http.MockHttpOutputMessage;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.within;
/**
* Jackson 3.x YAML converter tests.
*
* @author Sebastien Deleuze
*/
class JacksonYamlHttpMessageConverterTests {
private final JacksonYamlHttpMessageConverter converter = new JacksonYamlHttpMessageConverter();
private final YAMLMapper mapper = YAMLMapper.builder().build();
@Test
void canRead() {
assertThat(converter.canRead(MyBean.class, MediaType.APPLICATION_YAML)).isTrue();
assertThat(converter.canRead(MyBean.class, new MediaType("application", "json"))).isFalse();
assertThat(converter.canRead(MyBean.class, new MediaType("application", "xml"))).isFalse();
}
@Test
void canWrite() {
assertThat(converter.canWrite(MyBean.class, MediaType.APPLICATION_YAML)).isTrue();
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "json"))).isFalse();
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "xml"))).isFalse();
}
@Test
void read() throws IOException {
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});
MockHttpInputMessage inputMessage = new MockHttpInputMessage(mapper.writeValueAsBytes(body));
inputMessage.getHeaders().setContentType(MediaType.APPLICATION_YAML);
MyBean result = (MyBean) converter.read(MyBean.class, inputMessage);
assertThat(result.getString()).isEqualTo("Foo");
assertThat(result.getNumber()).isEqualTo(42);
assertThat(result.getFraction()).isCloseTo(42F, within(0F));
assertThat(result.getArray()).isEqualTo(new String[]{"Foo", "Bar"});
assertThat(result.isBool()).isTrue();
assertThat(result.getBytes()).isEqualTo(new byte[]{0x1, 0x2});
}
@Test
void write() 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, null, outputMessage);
assertThat(outputMessage.getBodyAsBytes()).isEqualTo(mapper.writeValueAsBytes(body));
assertThat(outputMessage.getHeaders().getContentType())
.as("Invalid content-type").isEqualTo(MediaType.APPLICATION_YAML);
}
public static class MyBean {
private String string;
private int number;
private float fraction;
private String[] array;
private boolean bool;
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;
}
public void setNumber(int number) {
this.number = number;
}
public float getFraction() {
return fraction;
}
public void setFraction(float fraction) {
this.fraction = fraction;
}
public String[] getArray() {
return array;
}
public void setArray(String[] array) {
this.array = array;
}
}
}

View File

@@ -522,7 +522,7 @@ class RestClientIntegrationTests {
expectRequestCount(1);
expectRequest(request -> {
assertThat(request.getPath()).isEqualTo("/pojo/capitalize");
assertThat(request.getBody().readUtf8()).isEqualTo("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}");
assertThat(request.getBody().readUtf8()).isEqualTo("{\"bar\":\"barbar\",\"foo\":\"foofoo\"}");
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
assertThat(request.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo("application/json");
});

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -30,6 +30,7 @@ import java.util.stream.Stream;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.annotation.JsonView;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.extension.TestExecutionExceptionHandler;
import org.junit.jupiter.params.ParameterizedTest;
@@ -407,7 +408,8 @@ class RestTemplateIntegrationTests extends AbstractMockWebServerTests {
}
@ParameterizedRestTemplateTest
void jsonPostForObjectWithJacksonView(ClientHttpRequestFactory clientHttpRequestFactory) {
@Disabled("Use RestClient + upcoming hint management instead")
void jsonPostForObjectWithJacksonJsonView(ClientHttpRequestFactory clientHttpRequestFactory) {
setUpClient(clientHttpRequestFactory);
HttpHeaders entityHeaders = new HttpHeaders();

View File

@@ -53,8 +53,8 @@ import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.SmartHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.util.DefaultUriBuilderFactory;
@@ -113,7 +113,7 @@ class RestTemplateTests {
RestTemplate restTemplate = new RestTemplate();
List<HttpMessageConverter<?>> httpMessageConverters = restTemplate.getMessageConverters();
assertThat(httpMessageConverters).extracting("class").containsOnlyOnce(
MappingJackson2HttpMessageConverter.class
JacksonJsonHttpMessageConverter.class
);
assertThat(httpMessageConverters).extracting("class").doesNotContain(
KotlinSerializationJsonHttpMessageConverter.class