supportedMediaTypes) {
+ super.setSupportedMediaTypes(supportedMediaTypes);
+ }
+
+ /**
+ * Return the main {@code ObjectMapper} in use.
+ */
+ public ObjectMapper getObjectMapper() {
+ return this.defaultObjectMapper;
+ }
+
+ /**
+ * Configure the {@link ObjectMapper} instances to use for the given
+ * {@link Class}. This is useful when you want to deviate from the
+ * {@link #getObjectMapper() default} ObjectMapper or have the
+ * {@code ObjectMapper} vary by {@code MediaType}.
+ * Note: Use of this method effectively turns off use of
+ * the default {@link #getObjectMapper() ObjectMapper} and
+ * {@link #setSupportedMediaTypes(List) supportedMediaTypes} for the given
+ * class. Therefore it is important for the mappings configured here to
+ * {@link MediaType#includes(MediaType) include} every MediaType that must
+ * be supported for the given class.
+ * @param clazz the type of Object to register ObjectMapper instances for
+ * @param registrar a consumer to populate or otherwise update the
+ * MediaType-to-ObjectMapper associations for the given Class
+ */
+ public void registerObjectMappersForType(Class> clazz, Consumer> registrar) {
+ if (this.objectMapperRegistrations == null) {
+ this.objectMapperRegistrations = new LinkedHashMap<>();
+ }
+ Map registrations =
+ this.objectMapperRegistrations.computeIfAbsent(clazz, c -> new LinkedHashMap<>());
+ registrar.accept(registrations);
+ }
+
+ /**
+ * Return ObjectMapper registrations for the given class, if any.
+ * @param clazz the class to look up for registrations for
+ * @return a map with registered MediaType-to-ObjectMapper registrations,
+ * or empty if in case of no registrations for the given class.
+ */
+ public Map getObjectMappersForType(Class> clazz) {
+ for (Map.Entry, Map> entry : getObjectMapperRegistrations().entrySet()) {
+ if (entry.getKey().isAssignableFrom(clazz)) {
+ return entry.getValue();
+ }
+ }
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public List getSupportedMediaTypes(Class> clazz) {
+ List result = null;
+ for (Map.Entry, Map> entry : getObjectMapperRegistrations().entrySet()) {
+ if (entry.getKey().isAssignableFrom(clazz)) {
+ result = (result != null ? result : new ArrayList<>(entry.getValue().size()));
+ result.addAll(entry.getValue().keySet());
+ }
+ }
+ if (!CollectionUtils.isEmpty(result)) {
+ return result;
+ }
+ return (ProblemDetail.class.isAssignableFrom(clazz) ?
+ getMediaTypesForProblemDetail() : getSupportedMediaTypes());
+ }
+
+ private Map, Map> getObjectMapperRegistrations() {
+ return (this.objectMapperRegistrations != null ? this.objectMapperRegistrations : Collections.emptyMap());
+ }
+
+ /**
+ * Return the supported media type(s) for {@link ProblemDetail}.
+ * By default, an empty list, unless overridden in subclasses.
+ */
+ protected List getMediaTypesForProblemDetail() {
+ return Collections.emptyList();
+ }
+
+
+ @Override
+ public boolean canRead(ResolvableType type, @Nullable MediaType mediaType) {
+ if (!canRead(mediaType)) {
+ return false;
+ }
+ Class> clazz = type.resolve();
+ if (clazz == null) {
+ return false;
+ }
+ return this.objectMapperRegistrations == null || selectObjectMapper(clazz, mediaType) != null;
+ }
+
+ @Override
+ public boolean canWrite(Class> clazz, @Nullable MediaType mediaType) {
+ if (!canWrite(mediaType)) {
+ return false;
+ }
+ if (mediaType != null && mediaType.getCharset() != null) {
+ Charset charset = mediaType.getCharset();
+ if (!ENCODINGS.containsKey(charset.name())) {
+ return false;
+ }
+ }
+ if (MappingJacksonValue.class.isAssignableFrom(clazz)) {
+ throw new UnsupportedOperationException("MappingJacksonValue is not supported, use hints instead");
+ }
+ return this.objectMapperRegistrations == null || selectObjectMapper(clazz, mediaType) != null;
+ }
+
+ /**
+ * Select an ObjectMapper to use, either the main ObjectMapper or another
+ * if the handling for the given Class has been customized through
+ * {@link #registerObjectMappersForType(Class, Consumer)}.
+ */
+ private @Nullable ObjectMapper selectObjectMapper(Class> targetType, @Nullable MediaType targetMediaType) {
+ if (targetMediaType == null || CollectionUtils.isEmpty(this.objectMapperRegistrations)) {
+ return this.defaultObjectMapper;
+ }
+ for (Map.Entry, Map> typeEntry : getObjectMapperRegistrations().entrySet()) {
+ if (typeEntry.getKey().isAssignableFrom(targetType)) {
+ for (Map.Entry objectMapperEntry : typeEntry.getValue().entrySet()) {
+ if (objectMapperEntry.getKey().includes(targetMediaType)) {
+ return objectMapperEntry.getValue();
+ }
+ }
+ // No matching registrations
+ return null;
+ }
+ }
+ // No registrations
+ return this.defaultObjectMapper;
+ }
+
+ @Override
+ public Object read(ResolvableType type, HttpInputMessage inputMessage, @Nullable Map hints)
+ throws IOException, HttpMessageNotReadableException {
+
+ Class> contextClass = (type.getSource() instanceof MethodParameter parameter ? parameter.getContainingClass() : null);
+ JavaType javaType = getJavaType(type.getType(), contextClass);
+ return readJavaType(javaType, inputMessage, hints);
+ }
+
+ @Override
+ protected Object readInternal(Class> clazz, HttpInputMessage inputMessage)
+ throws IOException, HttpMessageNotReadableException {
+
+ JavaType javaType = getJavaType(clazz, null);
+ return readJavaType(javaType, inputMessage, null);
+ }
+
+ private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage, @Nullable Map hints) throws IOException {
+ MediaType contentType = inputMessage.getHeaders().getContentType();
+ Charset charset = getCharset(contentType);
+
+ ObjectMapper objectMapper = selectObjectMapper(javaType.getRawClass(), contentType);
+ Assert.state(objectMapper != null, () -> "No ObjectMapper for " + javaType);
+
+ boolean isUnicode = ENCODINGS.containsKey(charset.name()) ||
+ "UTF-16".equals(charset.name()) ||
+ "UTF-32".equals(charset.name());
+ try {
+ InputStream inputStream = StreamUtils.nonClosing(inputMessage.getBody());
+ if (inputMessage instanceof MappingJacksonInputMessage) {
+ throw new UnsupportedOperationException("MappingJacksonInputMessage is not supported, use hints instead");
+ }
+ ObjectReader objectReader = objectMapper.readerFor(javaType);
+ if (hints != null && hints.containsKey(JSON_VIEW_HINT)) {
+ objectReader = objectReader.withView((Class>) hints.get(JSON_VIEW_HINT));
+ }
+ objectReader = customizeReader(objectReader, javaType);
+ if (isUnicode) {
+ return objectReader.readValue(inputStream);
+ }
+ else {
+ Reader reader = new InputStreamReader(inputStream, charset);
+ return objectReader.readValue(reader);
+ }
+ }
+ catch (InvalidDefinitionException ex) {
+ throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
+ }
+ catch (JacksonException ex) {
+ throw new HttpMessageNotReadableException("JSON parse error: " + ex.getOriginalMessage(), ex, inputMessage);
+ }
+ }
+
+ /**
+ * Subclasses can use this method to customize {@link ObjectReader} used
+ * for reading values.
+ * @param reader the reader instance to customize
+ * @param javaType the target type of element values to read to
+ * @return the customized {@link ObjectReader}
+ */
+ protected ObjectReader customizeReader(ObjectReader reader, JavaType javaType) {
+ return reader;
+ }
+
+ /**
+ * Determine the charset to use for JSON input.
+ * By default this is either the charset from the input {@code MediaType}
+ * or otherwise falling back on {@code UTF-8}. Can be overridden in subclasses.
+ * @param contentType the content type of the HTTP input message
+ * @return the charset to use
+ */
+ protected Charset getCharset(@Nullable MediaType contentType) {
+ if (contentType != null && contentType.getCharset() != null) {
+ return contentType.getCharset();
+ }
+ else {
+ return StandardCharsets.UTF_8;
+ }
+ }
+
+ @Override
+ protected void writeInternal(Object object, ResolvableType resolvableType, HttpOutputMessage outputMessage, @Nullable Map hints)
+ throws IOException, HttpMessageNotWritableException {
+
+ MediaType contentType = outputMessage.getHeaders().getContentType();
+ JsonEncoding encoding = getJsonEncoding(contentType);
+
+ Class> clazz = object.getClass();
+ ObjectMapper objectMapper = selectObjectMapper(clazz, contentType);
+ Assert.state(objectMapper != null, () -> "No ObjectMapper for " + clazz.getName());
+
+ OutputStream outputStream = StreamUtils.nonClosing(outputMessage.getBody());
+ Class> jsonView = null;
+ FilterProvider filters = null;
+ JavaType javaType = null;
+
+ Type type = resolvableType.getType();
+ if (TypeUtils.isAssignable(type, object.getClass())) {
+ javaType = getJavaType(type, null);
+ }
+ if (hints != null) {
+ jsonView = (Class>) hints.get(JSON_VIEW_HINT);
+ filters = (FilterProvider) hints.get(FILTER_PROVIDER_HINT);
+ }
+
+ ObjectWriter objectWriter = (jsonView != null ?
+ objectMapper.writerWithView(jsonView) : objectMapper.writer());
+ if (filters != null) {
+ objectWriter = objectWriter.with(filters);
+ }
+ if (javaType != null && (javaType.isContainerType() || javaType.isTypeOrSubTypeOf(Optional.class))) {
+ objectWriter = objectWriter.forType(javaType);
+ }
+ SerializationConfig config = objectWriter.getConfig();
+ if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) &&
+ config.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
+ objectWriter = objectWriter.with(this.ssePrettyPrinter);
+ }
+ objectWriter = customizeWriter(objectWriter, javaType, contentType);
+
+ try (JsonGenerator generator = objectWriter.createGenerator(outputStream, encoding)) {
+ writePrefix(generator, object);
+ objectWriter.writeValue(generator, object);
+ writeSuffix(generator, object);
+ generator.flush();
+ }
+ catch (InvalidDefinitionException ex) {
+ throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
+ }
+ catch (JacksonException ex) {
+ throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex);
+ }
+ }
+
+ /**
+ * Subclasses can use this method to customize {@link ObjectWriter} used
+ * for writing values.
+ * @param writer the writer instance to customize
+ * @param javaType the type of element values to write
+ * @param contentType the selected media type
+ * @return the customized {@link ObjectWriter}
+ */
+ protected ObjectWriter customizeWriter(
+ ObjectWriter writer, @Nullable JavaType javaType, @Nullable MediaType contentType) {
+
+ return writer;
+ }
+
+ /**
+ * Write a prefix before the main content.
+ * @param generator the generator to use for writing content.
+ * @param object the object to write to the output message.
+ */
+ protected void writePrefix(JsonGenerator generator, Object object) {
+ }
+
+ /**
+ * Write a suffix after the main content.
+ * @param generator the generator to use for writing content.
+ * @param object the object to write to the output message.
+ */
+ protected void writeSuffix(JsonGenerator generator, Object object) {
+ }
+
+ /**
+ * Return the Jackson {@link JavaType} for the specified type and context class.
+ * @param type the generic type to return the Jackson JavaType for
+ * @param contextClass a context class for the target type, for example a class
+ * in which the target type appears in a method signature (can be {@code null})
+ * @return the Jackson JavaType
+ */
+ protected JavaType getJavaType(Type type, @Nullable Class> contextClass) {
+ return this.defaultObjectMapper.constructType(GenericTypeResolver.resolveType(type, contextClass));
+ }
+
+ /**
+ * Determine the JSON encoding to use for the given content type.
+ * @param contentType the media type as requested by the caller
+ * @return the JSON encoding to use (never {@code null})
+ */
+ protected JsonEncoding getJsonEncoding(@Nullable MediaType contentType) {
+ if (contentType != null && contentType.getCharset() != null) {
+ Charset charset = contentType.getCharset();
+ JsonEncoding encoding = ENCODINGS.get(charset.name());
+ if (encoding != null) {
+ return encoding;
+ }
+ }
+ return JsonEncoding.UTF8;
+ }
+
+ @Override
+ protected boolean supportsRepeatableWrites(Object o) {
+ return true;
+ }
+}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/cbor/JacksonCborHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/cbor/JacksonCborHttpMessageConverter.java
new file mode 100644
index 00000000000..2c76703f667
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/converter/cbor/JacksonCborHttpMessageConverter.java
@@ -0,0 +1,61 @@
+/*
+ * 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 tools.jackson.databind.cfg.MapperBuilder;
+import tools.jackson.dataformat.cbor.CBORMapper;
+
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.AbstractJacksonHttpMessageConverter;
+
+/**
+ * Implementation of {@link org.springframework.http.converter.HttpMessageConverter
+ * HttpMessageConverter} that can read and write the CBOR
+ * data format using
+ * the dedicated Jackson 3.x extension .
+ *
+ * By default, this converter supports the {@link MediaType#APPLICATION_CBOR_VALUE}
+ * media type. This can be overridden by setting the {@link #setSupportedMediaTypes
+ * supportedMediaTypes} property.
+ *
+ *
The default constructor loads {@link tools.jackson.databind.JacksonModule}s
+ * found by {@link MapperBuilder#findModules(ClassLoader)}.
+ *
+ * @author Sebastien Deleuze
+ * @since 7.0
+ */
+public class JacksonCborHttpMessageConverter extends AbstractJacksonHttpMessageConverter {
+
+ /**
+ * Construct a new instance with a {@link CBORMapper} customized with the
+ * {@link tools.jackson.databind.JacksonModule}s found by
+ * {@link MapperBuilder#findModules(ClassLoader)}.
+ */
+ public JacksonCborHttpMessageConverter() {
+ super(CBORMapper.builder(), MediaType.APPLICATION_CBOR);
+ }
+
+ /**
+ * Construct a new instance with the provided {@link CBORMapper}.
+ * @see CBORMapper#builder()
+ * @see MapperBuilder#findAndAddModules(ClassLoader)
+ */
+ public JacksonCborHttpMessageConverter(CBORMapper mapper) {
+ super(mapper, MediaType.APPLICATION_CBOR);
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/JacksonJsonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/JacksonJsonHttpMessageConverter.java
new file mode 100644
index 00000000000..787bf1689b1
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/converter/json/JacksonJsonHttpMessageConverter.java
@@ -0,0 +1,124 @@
+/*
+ * 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.util.Collections;
+import java.util.List;
+
+import org.jspecify.annotations.Nullable;
+import tools.jackson.core.JsonGenerator;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.cfg.MapperBuilder;
+import tools.jackson.databind.json.JsonMapper;
+
+import org.springframework.http.MediaType;
+import org.springframework.http.ProblemDetail;
+import org.springframework.http.converter.AbstractJacksonHttpMessageConverter;
+
+/**
+ * Implementation of {@link org.springframework.http.converter.HttpMessageConverter}
+ * that can read and write JSON using Jackson 3.x's
+ * {@link ObjectMapper}.
+ *
+ *
This converter can be used to bind to typed beans, or untyped
+ * {@code HashMap} instances.
+ *
+ *
By default, this converter supports {@code application/json} and
+ * {@code application/*+json} with {@code UTF-8} character set. This
+ * can be overridden by setting the {@link #setSupportedMediaTypes supportedMediaTypes}
+ * property.
+ *
+ *
The default constructor loads {@link tools.jackson.databind.JacksonModule}s
+ * found by {@link MapperBuilder#findModules(ClassLoader)}.
+ *
+ *
The following hints entries are supported:
+ *
+ * A JSON view with a com.fasterxml.jackson.annotation.JsonView
+ * key and the class name of the JSON view as value.
+ * A filter provider with a tools.jackson.databind.ser.FilterProvider
+ * key and the filter provider class name as value.
+ *
+ *
+ * @author Sebastien Deleuze
+ * @since 7.0
+ */
+public class JacksonJsonHttpMessageConverter extends AbstractJacksonHttpMessageConverter {
+
+ private static final List problemDetailMediaTypes =
+ Collections.singletonList(MediaType.APPLICATION_PROBLEM_JSON);
+
+ private static final MediaType[] DEFAULT_JSON_MIME_TYPES = new MediaType[] {
+ MediaType.APPLICATION_JSON, new MediaType("application", "*+json") };
+
+
+ private @Nullable String jsonPrefix;
+
+
+ /**
+ * Construct a new instance with a {@link JsonMapper} customized with the
+ * {@link tools.jackson.databind.JacksonModule}s found by
+ * {@link MapperBuilder#findModules(ClassLoader)} and
+ * {@link ProblemDetailJacksonMixin}.
+ */
+ public JacksonJsonHttpMessageConverter() {
+ super(JsonMapper.builder().addMixIn(ProblemDetail.class, ProblemDetailJacksonMixin.class), DEFAULT_JSON_MIME_TYPES);
+ }
+
+ /**
+ * Construct a new instance with the provided {@link ObjectMapper}.
+ * @see JsonMapper#builder()
+ * @see MapperBuilder#findModules(ClassLoader)
+ */
+ public JacksonJsonHttpMessageConverter(ObjectMapper objectMapper) {
+ super(objectMapper, DEFAULT_JSON_MIME_TYPES);
+ }
+
+
+ /**
+ * Specify a custom prefix to use for this view's JSON output.
+ * Default is none.
+ * @see #setPrefixJson
+ */
+ public void setJsonPrefix(String jsonPrefix) {
+ this.jsonPrefix = jsonPrefix;
+ }
+
+ /**
+ * Indicate whether the JSON output by this view should be prefixed with ")]}', ". Default is {@code false}.
+ * Prefixing the JSON string in this manner is used to help prevent JSON Hijacking.
+ * The prefix renders the string syntactically invalid as a script so that it cannot be hijacked.
+ * This prefix should be stripped before parsing the string as JSON.
+ * @see #setJsonPrefix
+ */
+ public void setPrefixJson(boolean prefixJson) {
+ this.jsonPrefix = (prefixJson ? ")]}', " : null);
+ }
+
+
+ @Override
+ protected List getMediaTypesForProblemDetail() {
+ return problemDetailMediaTypes;
+ }
+
+ @Override
+ protected void writePrefix(JsonGenerator generator, Object object) {
+ if (this.jsonPrefix != null) {
+ generator.writeRaw(this.jsonPrefix);
+ }
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/smile/JacksonSmileHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/smile/JacksonSmileHttpMessageConverter.java
new file mode 100644
index 00000000000..7a8ee20cb27
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/converter/smile/JacksonSmileHttpMessageConverter.java
@@ -0,0 +1,63 @@
+/*
+ * 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 tools.jackson.databind.cfg.MapperBuilder;
+import tools.jackson.dataformat.smile.SmileMapper;
+
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.AbstractJacksonHttpMessageConverter;
+
+/**
+ * Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter}
+ * that can read and write Smile data format ("binary JSON") using
+ *
+ * the dedicated Jackson 3.x extension .
+ *
+ * By default, this converter supports {@code "application/x-jackson-smile"}
+ * media type. This can be overridden by setting the
+ * {@link #setSupportedMediaTypes supportedMediaTypes} property.
+ *
+ *
The default constructor loads {@link tools.jackson.databind.JacksonModule}s
+ * found by {@link MapperBuilder#findModules(ClassLoader)}.
+ *
+ * @author Sebastien Deleuze
+ * @since 7.0
+ */
+public class JacksonSmileHttpMessageConverter extends AbstractJacksonHttpMessageConverter {
+
+ private static final MediaType DEFAULT_SMILE_MIME_TYPES = new MediaType("application", "x-jackson-smile");
+
+ /**
+ * Construct a new instance with a {@link SmileMapper} customized with the
+ * {@link tools.jackson.databind.JacksonModule}s found by
+ * {@link MapperBuilder#findModules(ClassLoader)}.
+ */
+ public JacksonSmileHttpMessageConverter() {
+ super(SmileMapper.builder(), DEFAULT_SMILE_MIME_TYPES);
+ }
+
+ /**
+ * Construct a new instance with the provided {@link SmileMapper}.
+ * @see SmileMapper#builder()
+ * @see MapperBuilder#findAndAddModules(ClassLoader)
+ */
+ public JacksonSmileHttpMessageConverter(SmileMapper mapper) {
+ super(mapper, DEFAULT_SMILE_MIME_TYPES);
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java
index 31053bfe174..9c29df735cb 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java
@@ -17,16 +17,21 @@
package org.springframework.http.converter.support;
import org.springframework.http.converter.FormHttpMessageConverter;
+import org.springframework.http.converter.cbor.JacksonCborHttpMessageConverter;
import org.springframework.http.converter.cbor.KotlinSerializationCborHttpMessageConverter;
import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
+import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.protobuf.KotlinSerializationProtobufHttpMessageConverter;
+import org.springframework.http.converter.smile.JacksonSmileHttpMessageConverter;
import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter;
+import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
+import org.springframework.http.converter.yaml.JacksonYamlHttpMessageConverter;
import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter;
import org.springframework.util.ClassUtils;
@@ -44,14 +49,24 @@ public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConv
private static final boolean jaxb2Present;
+ private static final boolean jacksonPresent;
+
private static final boolean jackson2Present;
+ private static final boolean jacksonXmlPresent;
+
private static final boolean jackson2XmlPresent;
+ private static final boolean jacksonSmilePresent;
+
private static final boolean jackson2SmilePresent;
+ private static final boolean jacksonCborPresent;
+
private static final boolean jackson2CborPresent;
+ private static final boolean jacksonYamlPresent;
+
private static final boolean jackson2YamlPresent;
private static final boolean gsonPresent;
@@ -67,12 +82,17 @@ public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConv
static {
ClassLoader classLoader = AllEncompassingFormHttpMessageConverter.class.getClassLoader();
jaxb2Present = ClassUtils.isPresent("jakarta.xml.bind.Binder", classLoader);
+ jacksonPresent = ClassUtils.isPresent("tools.jackson.databind.ObjectMapper", classLoader);
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
- jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
- jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
- jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
- jackson2YamlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", classLoader);
+ jacksonXmlPresent = jacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.xml.XmlMapper", classLoader);
+ jackson2XmlPresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
+ jacksonSmilePresent = jacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.smile.SmileMapper", classLoader);
+ jackson2SmilePresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
+ jacksonCborPresent = jacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.cbor.CBORMapper", classLoader);
+ jackson2CborPresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
+ jacksonYamlPresent = jacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.yaml.YAMLMapper", classLoader);
+ jackson2YamlPresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader);
kotlinSerializationCborPresent = ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader);
@@ -83,11 +103,14 @@ public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConv
public AllEncompassingFormHttpMessageConverter() {
- if (jaxb2Present && !jackson2XmlPresent) {
+ if (jaxb2Present && !jacksonXmlPresent && !jackson2XmlPresent) {
addPartConverter(new Jaxb2RootElementHttpMessageConverter());
}
- if (jackson2Present) {
+ if (jacksonPresent) {
+ addPartConverter(new JacksonJsonHttpMessageConverter());
+ }
+ else if (jackson2Present) {
addPartConverter(new MappingJackson2HttpMessageConverter());
}
else if (gsonPresent) {
@@ -100,22 +123,34 @@ public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConv
addPartConverter(new KotlinSerializationJsonHttpMessageConverter());
}
- if (jackson2XmlPresent) {
+ if (jacksonXmlPresent) {
+ addPartConverter(new JacksonXmlHttpMessageConverter());
+ }
+ else if (jackson2XmlPresent) {
addPartConverter(new MappingJackson2XmlHttpMessageConverter());
}
- if (jackson2SmilePresent) {
+ if (jacksonSmilePresent) {
+ addPartConverter(new JacksonSmileHttpMessageConverter());
+ }
+ else if (jackson2SmilePresent) {
addPartConverter(new MappingJackson2SmileHttpMessageConverter());
}
- if (jackson2CborPresent) {
+ if (jacksonCborPresent) {
+ addPartConverter(new JacksonCborHttpMessageConverter());
+ }
+ else if (jackson2CborPresent) {
addPartConverter(new MappingJackson2CborHttpMessageConverter());
}
else if (kotlinSerializationCborPresent) {
addPartConverter(new KotlinSerializationCborHttpMessageConverter());
}
- if (jackson2YamlPresent) {
+ if (jacksonYamlPresent) {
+ addPartConverter(new JacksonYamlHttpMessageConverter());
+ }
+ else if (jackson2YamlPresent) {
addPartConverter(new MappingJackson2YamlHttpMessageConverter());
}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/JacksonXmlHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/JacksonXmlHttpMessageConverter.java
new file mode 100644
index 00000000000..840e154ef12
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/converter/xml/JacksonXmlHttpMessageConverter.java
@@ -0,0 +1,111 @@
+/*
+ * 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.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+
+import tools.jackson.databind.cfg.MapperBuilder;
+import tools.jackson.dataformat.xml.XmlFactory;
+import tools.jackson.dataformat.xml.XmlMapper;
+
+import org.springframework.http.MediaType;
+import org.springframework.http.ProblemDetail;
+import org.springframework.http.converter.AbstractJacksonHttpMessageConverter;
+import org.springframework.http.converter.json.ProblemDetailJacksonXmlMixin;
+import org.springframework.util.xml.StaxUtils;
+
+/**
+ * Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter}
+ * that can read and write XML using
+ * Jackson 3.x extension component for reading and writing XML encoded data .
+ *
+ *
By default, this converter supports {@code application/xml}, {@code text/xml}, and
+ * {@code application/*+xml} with {@code UTF-8} character set. This can be overridden by
+ * setting the {@link #setSupportedMediaTypes supportedMediaTypes} property.
+ *
+ *
The default constructor loads {@link tools.jackson.databind.JacksonModule}s
+ * found by {@link MapperBuilder#findModules(ClassLoader)}.
+ *
+ *
The following hint entries are supported:
+ *
+ * A JSON view with a com.fasterxml.jackson.annotation.JsonView
+ * key and the class name of the JSON view as value.
+ * A filter provider with a tools.jackson.databind.ser.FilterProvider
+ * key and the filter provider class name as value.
+ *
+ *
+ * @author Sebastien Deleuze
+ * @since 7.0
+ */
+public class JacksonXmlHttpMessageConverter extends AbstractJacksonHttpMessageConverter {
+
+ private static final List problemDetailMediaTypes =
+ Collections.singletonList(MediaType.APPLICATION_PROBLEM_XML);
+
+ private static final MediaType[] DEFAULT_XML_MIME_TYPES = new MediaType[] {
+ new MediaType("application", "xml", StandardCharsets.UTF_8),
+ new MediaType("text", "xml", StandardCharsets.UTF_8),
+ new MediaType("application", "*+xml", StandardCharsets.UTF_8)
+ };
+
+ /**
+ * Construct a new instance with a {@link XmlMapper} created from
+ * {@link #defensiveXmlFactory} and customized with the
+ * {@link tools.jackson.databind.JacksonModule}s found by
+ * {@link MapperBuilder#findModules(ClassLoader)} and
+ * {@link ProblemDetailJacksonXmlMixin}.
+ */
+ public JacksonXmlHttpMessageConverter() {
+ this(XmlMapper.builder(defensiveXmlFactory()));
+ }
+
+ /**
+ * Construct a new instance with the provided {@link XmlMapper.Builder builder}
+ * customized with the {@link tools.jackson.databind.JacksonModule}s found by
+ * {@link MapperBuilder#findModules(ClassLoader)} and
+ * {@link ProblemDetailJacksonXmlMixin}.
+ */
+ public JacksonXmlHttpMessageConverter(XmlMapper.Builder builder) {
+ super(builder.addMixIn(ProblemDetail.class, ProblemDetailJacksonXmlMixin.class), DEFAULT_XML_MIME_TYPES);
+ }
+
+ /**
+ * Construct a new instance with the provided {@link XmlMapper}.
+ * @see XmlMapper#builder()
+ * @see MapperBuilder#findModules(ClassLoader)
+ */
+ public JacksonXmlHttpMessageConverter(XmlMapper xmlMapper) {
+ super(xmlMapper, DEFAULT_XML_MIME_TYPES);
+ }
+
+ /**
+ * Return an {@link XmlFactory} created from {@link StaxUtils#createDefensiveInputFactory}
+ * with Spring's defensive setup, i.e. no support for the resolution of DTDs and external
+ * entities.
+ */
+ public static XmlFactory defensiveXmlFactory() {
+ return new XmlFactory(StaxUtils.createDefensiveInputFactory());
+ }
+
+ @Override
+ protected List getMediaTypesForProblemDetail() {
+ return problemDetailMediaTypes;
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/yaml/JacksonYamlHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/yaml/JacksonYamlHttpMessageConverter.java
new file mode 100644
index 00000000000..f5b6aa7e26d
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/converter/yaml/JacksonYamlHttpMessageConverter.java
@@ -0,0 +1,61 @@
+/*
+ * 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 tools.jackson.databind.cfg.MapperBuilder;
+import tools.jackson.dataformat.yaml.YAMLMapper;
+
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.AbstractJacksonHttpMessageConverter;
+
+/**
+ * Implementation of {@link org.springframework.http.converter.HttpMessageConverter
+ * HttpMessageConverter} that can read and write the YAML
+ * data format using
+ * the dedicated Jackson 3.x extension .
+ *
+ * By default, this converter supports the {@link MediaType#APPLICATION_YAML_VALUE}
+ * media type. This can be overridden by setting the {@link #setSupportedMediaTypes
+ * supportedMediaTypes} property.
+ *
+ *
The default constructor loads {@link tools.jackson.databind.JacksonModule}s
+ * found by {@link MapperBuilder#findModules(ClassLoader)}.
+ *
+ * @author Sebastien Deleuze
+ * @since 7.0
+ */
+public class JacksonYamlHttpMessageConverter extends AbstractJacksonHttpMessageConverter {
+
+ /**
+ * Construct a new instance with a {@link YAMLMapper} customized with the
+ * {@link tools.jackson.databind.JacksonModule}s found by
+ * {@link MapperBuilder#findModules(ClassLoader)}.
+ */
+ public JacksonYamlHttpMessageConverter() {
+ super(YAMLMapper.builder(), MediaType.APPLICATION_YAML);
+ }
+
+ /**
+ * Construct a new instance with the provided {@link YAMLMapper}.
+ * @see YAMLMapper#builder()
+ * @see MapperBuilder#findAndAddModules(ClassLoader)
+ */
+ public JacksonYamlHttpMessageConverter(YAMLMapper mapper) {
+ super(mapper, MediaType.APPLICATION_YAML);
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java
index f59a4aa1537..9e83bb5e78f 100644
--- a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java
+++ b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java
@@ -47,19 +47,24 @@ import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
+import org.springframework.http.converter.cbor.JacksonCborHttpMessageConverter;
import org.springframework.http.converter.cbor.KotlinSerializationCborHttpMessageConverter;
import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter;
import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter;
import org.springframework.http.converter.feed.RssChannelHttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
+import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.protobuf.KotlinSerializationProtobufHttpMessageConverter;
+import org.springframework.http.converter.smile.JacksonSmileHttpMessageConverter;
import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
+import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
+import org.springframework.http.converter.yaml.JacksonYamlHttpMessageConverter;
import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -96,14 +101,24 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
private static final boolean jaxb2Present;
+ private static final boolean jacksonPresent;
+
private static final boolean jackson2Present;
+ private static final boolean jacksonXmlPresent;
+
private static final boolean jackson2XmlPresent;
+ private static final boolean jacksonSmilePresent;
+
private static final boolean jackson2SmilePresent;
+ private static final boolean jacksonCborPresent;
+
private static final boolean jackson2CborPresent;
+ private static final boolean jacksonYamlPresent;
+
private static final boolean jackson2YamlPresent;
private static final boolean gsonPresent;
@@ -126,12 +141,17 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", loader);
jaxb2Present = ClassUtils.isPresent("jakarta.xml.bind.Binder", loader);
+ jacksonPresent = ClassUtils.isPresent("tools.jackson.databind.ObjectMapper", loader);
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", loader) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", loader);
- jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", loader);
- jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", loader);
- jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", loader);
- jackson2YamlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", loader);
+ jacksonXmlPresent = jacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.xml.XmlMapper", loader);
+ jackson2XmlPresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", loader);
+ jacksonSmilePresent = jacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.smile.SmileMapper", loader);
+ jackson2SmilePresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", loader);
+ jacksonCborPresent = jacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.cbor.CBORMapper", loader);
+ jackson2CborPresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", loader);
+ jacksonYamlPresent = jacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.yaml.YAMLMapper", loader);
+ jackson2YamlPresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", loader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", loader);
jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", loader);
kotlinSerializationCborPresent = ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", loader);
@@ -463,7 +483,10 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
this.messageConverters.add(new RssChannelHttpMessageConverter());
}
- if (jackson2XmlPresent) {
+ if (jacksonXmlPresent) {
+ this.messageConverters.add(new JacksonXmlHttpMessageConverter());
+ }
+ else if (jackson2XmlPresent) {
this.messageConverters.add(new MappingJackson2XmlHttpMessageConverter());
}
else if (jaxb2Present) {
@@ -474,7 +497,10 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
this.messageConverters.add(new KotlinSerializationProtobufHttpMessageConverter());
}
- if (jackson2Present) {
+ if (jacksonPresent) {
+ this.messageConverters.add(new JacksonJsonHttpMessageConverter());
+ }
+ else if (jackson2Present) {
this.messageConverters.add(new MappingJackson2HttpMessageConverter());
}
else if (gsonPresent) {
@@ -487,18 +513,27 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
this.messageConverters.add(new KotlinSerializationJsonHttpMessageConverter());
}
+ if (jacksonSmilePresent) {
+ this.messageConverters.add(new JacksonSmileHttpMessageConverter());
+ }
if (jackson2SmilePresent) {
this.messageConverters.add(new MappingJackson2SmileHttpMessageConverter());
}
- if (jackson2CborPresent) {
+ if (jacksonCborPresent) {
+ this.messageConverters.add(new JacksonCborHttpMessageConverter());
+ }
+ else if (jackson2CborPresent) {
this.messageConverters.add(new MappingJackson2CborHttpMessageConverter());
}
else if (kotlinSerializationCborPresent) {
this.messageConverters.add(new KotlinSerializationCborHttpMessageConverter());
}
- if (jackson2YamlPresent) {
+ if (jacksonYamlPresent) {
+ this.messageConverters.add(new JacksonYamlHttpMessageConverter());
+ }
+ else if (jackson2YamlPresent) {
this.messageConverters.add(new MappingJackson2YamlHttpMessageConverter());
}
}
diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
index 674c3c80622..a5f7a692a32 100644
--- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
+++ b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
@@ -56,19 +56,24 @@ import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.SmartHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
+import org.springframework.http.converter.cbor.JacksonCborHttpMessageConverter;
import org.springframework.http.converter.cbor.KotlinSerializationCborHttpMessageConverter;
import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter;
import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter;
import org.springframework.http.converter.feed.RssChannelHttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
+import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.protobuf.KotlinSerializationProtobufHttpMessageConverter;
+import org.springframework.http.converter.smile.JacksonSmileHttpMessageConverter;
import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
+import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
+import org.springframework.http.converter.yaml.JacksonYamlHttpMessageConverter;
import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -124,14 +129,24 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
private static final boolean jaxb2Present;
+ private static final boolean jacksonPresent;
+
private static final boolean jackson2Present;
+ private static final boolean jacksonXmlPresent;
+
private static final boolean jackson2XmlPresent;
+ private static final boolean jacksonSmilePresent;
+
private static final boolean jackson2SmilePresent;
+ private static final boolean jacksonCborPresent;
+
private static final boolean jackson2CborPresent;
+ private static final boolean jacksonYamlPresent;
+
private static final boolean jackson2YamlPresent;
private static final boolean gsonPresent;
@@ -151,12 +166,17 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader);
jaxb2Present = ClassUtils.isPresent("jakarta.xml.bind.Binder", classLoader);
+ jacksonPresent = ClassUtils.isPresent("tools.jackson.databind.ObjectMapper", classLoader);
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
- jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
- jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
- jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
- jackson2YamlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", classLoader);
+ jacksonXmlPresent = jacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.xml.XmlMapper", classLoader);
+ jackson2XmlPresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
+ jacksonSmilePresent = jacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.smile.SmileMapper", classLoader);
+ jackson2SmilePresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
+ jacksonCborPresent = jacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.cbor.CBORMapper", classLoader);
+ jackson2CborPresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
+ jacksonYamlPresent = jacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.yaml.YAMLMapper", classLoader);
+ jackson2YamlPresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader);
kotlinSerializationCborPresent = ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader);
@@ -193,7 +213,10 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
this.messageConverters.add(new RssChannelHttpMessageConverter());
}
- if (jackson2XmlPresent) {
+ if (jacksonXmlPresent) {
+ this.messageConverters.add(new JacksonXmlHttpMessageConverter());
+ }
+ else if (jackson2XmlPresent) {
this.messageConverters.add(new MappingJackson2XmlHttpMessageConverter());
}
else if (jaxb2Present) {
@@ -204,7 +227,10 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
this.messageConverters.add(new KotlinSerializationProtobufHttpMessageConverter());
}
- if (jackson2Present) {
+ if (jacksonPresent) {
+ this.messageConverters.add(new JacksonJsonHttpMessageConverter());
+ }
+ else if (jackson2Present) {
this.messageConverters.add(new MappingJackson2HttpMessageConverter());
}
else if (gsonPresent) {
@@ -217,18 +243,27 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
this.messageConverters.add(new KotlinSerializationJsonHttpMessageConverter());
}
- if (jackson2SmilePresent) {
+ if (jacksonSmilePresent) {
+ this.messageConverters.add(new JacksonSmileHttpMessageConverter());
+ }
+ else if (jackson2SmilePresent) {
this.messageConverters.add(new MappingJackson2SmileHttpMessageConverter());
}
- if (jackson2CborPresent) {
+ if (jacksonCborPresent) {
+ this.messageConverters.add(new JacksonCborHttpMessageConverter());
+ }
+ else if (jackson2CborPresent) {
this.messageConverters.add(new MappingJackson2CborHttpMessageConverter());
}
else if (kotlinSerializationCborPresent) {
this.messageConverters.add(new KotlinSerializationCborHttpMessageConverter());
}
- if (jackson2YamlPresent) {
+ if (jacksonYamlPresent) {
+ this.messageConverters.add(new JacksonYamlHttpMessageConverter());
+ }
+ else if (jackson2YamlPresent) {
this.messageConverters.add(new MappingJackson2YamlHttpMessageConverter());
}
diff --git a/spring-web/src/test/java/org/springframework/http/converter/cbor/JacksonCborHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/cbor/JacksonCborHttpMessageConverterTests.java
new file mode 100644
index 00000000000..288718750ac
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/http/converter/cbor/JacksonCborHttpMessageConverterTests.java
@@ -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;
+ }
+ }
+
+}
diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/JacksonJsonHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/JacksonJsonHttpMessageConverterTests.java
new file mode 100644
index 00000000000..86fe09d6960
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/http/converter/json/JacksonJsonHttpMessageConverterTests.java
@@ -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 result = (HashMap) 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 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 results = (List) 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> 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 results = (List) 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> 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(StandardCharsets.UTF_8));
+ inputMessage.getHeaders().setContentType(MediaType.APPLICATION_JSON);
+
+ MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
+ List results = (List) 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> optionalParent = new ParameterizedTypeReference<>() {};
+ Optional 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 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 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 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 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> 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 result = (HashMap) 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 result = (HashMap) this.converter.read(HashMap.class, inputMessage);
+
+ assertThat(result).containsExactly(entry("foo", "bar"));
+ }
+
+ @Test
+ void writeAscii() throws Exception {
+ MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
+ Map 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);
+ }
+ }
+
+}
diff --git a/spring-web/src/test/java/org/springframework/http/converter/smile/JacksonSmileHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/smile/JacksonSmileHttpMessageConverterTests.java
new file mode 100644
index 00000000000..09fa326ea00
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/http/converter/smile/JacksonSmileHttpMessageConverterTests.java
@@ -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;
+ }
+ }
+
+}
diff --git a/spring-web/src/test/java/org/springframework/http/converter/xml/JacksonXmlHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/xml/JacksonXmlHttpMessageConverterTests.java
new file mode 100644
index 00000000000..580fa7fc9e9
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/http/converter/xml/JacksonXmlHttpMessageConverterTests.java
@@ -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 = "" +
+ "Foo " +
+ "42 " +
+ "42.0 " +
+ "Foo " +
+ "Bar " +
+ "true " +
+ "AQI= ";
+ 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("Foo ");
+ assertThat(result).contains("42 ");
+ assertThat(result).contains("42.0 ");
+ assertThat(result).contains("Foo Bar ");
+ assertThat(result).contains("true ");
+ assertThat(result).contains("AQI= ");
+ 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 = "string value ";
+ 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 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("with ");
+ assertThat(result).doesNotContain("with ");
+ assertThat(result).doesNotContain("without ");
+ }
+
+ @Test
+ void customXmlMapper() {
+ new JacksonXmlHttpMessageConverter(new MyXmlMapper());
+ // Assert no exception is thrown
+ }
+
+ @Test
+ void readWithExternalReference() throws IOException {
+ String body = "\n" +
+ " ]>&ext; ";
+
+ 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 = """
+
+
+
+
+
+
+
+
+
+
+
+
+ ]>
+ &lol9; """;
+
+ 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 = "" +
+ "føø bår " +
+ " ";
+
+ 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 {
+ }
+
+}
diff --git a/spring-web/src/test/java/org/springframework/http/converter/yaml/JacksonYamlHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/yaml/JacksonYamlHttpMessageConverterTests.java
new file mode 100644
index 00000000000..7f0fbd95450
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/http/converter/yaml/JacksonYamlHttpMessageConverterTests.java
@@ -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;
+ }
+ }
+
+}
diff --git a/spring-web/src/test/java/org/springframework/web/client/RestClientIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/client/RestClientIntegrationTests.java
index c675334f92a..dced302e5b0 100644
--- a/spring-web/src/test/java/org/springframework/web/client/RestClientIntegrationTests.java
+++ b/spring-web/src/test/java/org/springframework/web/client/RestClientIntegrationTests.java
@@ -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");
});
diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java
index 4a595825229..a0b9e63505a 100644
--- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java
+++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java
@@ -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();
diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java
index fcf750abd26..4c421a56bde 100644
--- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java
+++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java
@@ -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> httpMessageConverters = restTemplate.getMessageConverters();
assertThat(httpMessageConverters).extracting("class").containsOnlyOnce(
- MappingJackson2HttpMessageConverter.class
+ JacksonJsonHttpMessageConverter.class
);
assertThat(httpMessageConverters).extracting("class").doesNotContain(
KotlinSerializationJsonHttpMessageConverter.class
diff --git a/spring-webmvc/spring-webmvc.gradle b/spring-webmvc/spring-webmvc.gradle
index 7f4aabc0d1d..e26677cc6ff 100644
--- a/spring-webmvc/spring-webmvc.gradle
+++ b/spring-webmvc/spring-webmvc.gradle
@@ -39,6 +39,12 @@ dependencies {
optional("org.jetbrains.kotlinx:kotlinx-serialization-protobuf")
optional("org.reactivestreams:reactive-streams")
optional("org.webjars:webjars-locator-lite")
+ optional("tools.jackson.core:jackson-databind")
+ optional("tools.jackson.dataformat:jackson-dataformat-smile")
+ optional("tools.jackson.dataformat:jackson-dataformat-cbor")
+ optional("tools.jackson.dataformat:jackson-dataformat-smile")
+ optional("tools.jackson.dataformat:jackson-dataformat-xml")
+ optional("tools.jackson.dataformat:jackson-dataformat-yaml")
testCompileOnly("com.google.code.findbugs:findbugs") { // for groovy-templates
exclude group: "dom4j", module: "dom4j"
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java
index 8d031b7990a..72bc6fd107d 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java
@@ -45,20 +45,25 @@ import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.ResourceRegionHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
+import org.springframework.http.converter.cbor.JacksonCborHttpMessageConverter;
import org.springframework.http.converter.cbor.KotlinSerializationCborHttpMessageConverter;
import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter;
import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter;
import org.springframework.http.converter.feed.RssChannelHttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
+import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.protobuf.KotlinSerializationProtobufHttpMessageConverter;
+import org.springframework.http.converter.smile.JacksonSmileHttpMessageConverter;
import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
+import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
+import org.springframework.http.converter.yaml.JacksonYamlHttpMessageConverter;
import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
@@ -196,14 +201,24 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
private static final boolean jaxb2Present;
+ private static final boolean jacksonPresent;
+
private static final boolean jackson2Present;
+ private static final boolean jacksonXmlPresent;
+
private static final boolean jackson2XmlPresent;
+ private static final boolean jacksonSmilePresent;
+
private static final boolean jackson2SmilePresent;
+ private static final boolean jacksonCborPresent;
+
private static final boolean jackson2CborPresent;
+ private static final boolean jacksonYamlPresent;
+
private static final boolean jackson2YamlPresent;
private static final boolean gsonPresent;
@@ -220,12 +235,17 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
ClassLoader classLoader = WebMvcConfigurationSupport.class.getClassLoader();
romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader);
jaxb2Present = ClassUtils.isPresent("jakarta.xml.bind.Binder", classLoader);
+ jacksonPresent = ClassUtils.isPresent("tools.jackson.databind.ObjectMapper", classLoader);
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
- jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
- jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
- jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
- jackson2YamlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", classLoader);
+ jacksonXmlPresent = jacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.xml.XmlMapper", classLoader);
+ jackson2XmlPresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
+ jacksonSmilePresent = jacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.smile.SmileMapper", classLoader);
+ jackson2SmilePresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
+ jacksonCborPresent = jacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.cbor.CBORMapper", classLoader);
+ jackson2CborPresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
+ jacksonYamlPresent = jacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.yaml.YAMLMapper", classLoader);
+ jackson2YamlPresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader);
kotlinSerializationCborPresent = ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader);
@@ -445,19 +465,19 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
map.put("atom", MediaType.APPLICATION_ATOM_XML);
map.put("rss", MediaType.APPLICATION_RSS_XML);
}
- if (jaxb2Present || jackson2XmlPresent) {
+ if (jaxb2Present || jacksonXmlPresent || jackson2XmlPresent) {
map.put("xml", MediaType.APPLICATION_XML);
}
- if (jackson2Present || gsonPresent || jsonbPresent || kotlinSerializationJsonPresent) {
+ if (jacksonPresent || jackson2Present || gsonPresent || jsonbPresent || kotlinSerializationJsonPresent) {
map.put("json", MediaType.APPLICATION_JSON);
}
- if (jackson2SmilePresent) {
+ if (jacksonSmilePresent || jackson2SmilePresent) {
map.put("smile", MediaType.valueOf("application/x-jackson-smile"));
}
- if (jackson2CborPresent || kotlinSerializationCborPresent) {
+ if (jacksonCborPresent || jackson2CborPresent || kotlinSerializationCborPresent) {
map.put("cbor", MediaType.APPLICATION_CBOR);
}
- if (jackson2YamlPresent) {
+ if (jacksonYamlPresent || jackson2YamlPresent) {
map.put("yaml", MediaType.APPLICATION_YAML);
}
return map;
@@ -679,7 +699,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
adapter.setCustomReturnValueHandlers(getReturnValueHandlers());
adapter.setErrorResponseInterceptors(getErrorResponseInterceptors());
- if (jackson2Present) {
+ if (jacksonPresent || jackson2Present) {
adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice()));
adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice()));
}
@@ -909,7 +929,10 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
messageConverters.add(new RssChannelHttpMessageConverter());
}
- if (jackson2XmlPresent) {
+ if (jacksonXmlPresent) {
+ messageConverters.add(new JacksonXmlHttpMessageConverter());
+ }
+ else if (jackson2XmlPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
@@ -924,7 +947,10 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
messageConverters.add(new KotlinSerializationProtobufHttpMessageConverter());
}
- if (jackson2Present) {
+ if (jacksonPresent) {
+ messageConverters.add(new JacksonJsonHttpMessageConverter());
+ }
+ else if (jackson2Present) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
@@ -941,14 +967,21 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
messageConverters.add(new KotlinSerializationJsonHttpMessageConverter());
}
- if (jackson2SmilePresent) {
+ if (jacksonSmilePresent) {
+ messageConverters.add(new JacksonSmileHttpMessageConverter());
+ }
+ else if (jackson2SmilePresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2SmileHttpMessageConverter(builder.build()));
}
- if (jackson2CborPresent) {
+
+ if (jacksonCborPresent) {
+ messageConverters.add(new JacksonCborHttpMessageConverter());
+ }
+ else if (jackson2CborPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.cbor();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
@@ -958,7 +991,11 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
else if (kotlinSerializationCborPresent) {
messageConverters.add(new KotlinSerializationCborHttpMessageConverter());
}
- if (jackson2YamlPresent) {
+
+ if (jacksonYamlPresent) {
+ messageConverters.add(new JacksonYamlHttpMessageConverter());
+ }
+ else if (jackson2YamlPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.yaml();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
@@ -1084,7 +1121,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
exceptionHandlerResolver.setCustomArgumentResolvers(getArgumentResolvers());
exceptionHandlerResolver.setCustomReturnValueHandlers(getReturnValueHandlers());
exceptionHandlerResolver.setErrorResponseInterceptors(getErrorResponseInterceptors());
- if (jackson2Present) {
+ if (jacksonPresent || jackson2Present) {
exceptionHandlerResolver.setResponseBodyAdvice(
Collections.singletonList(new JsonViewResponseBodyAdvice()));
}
diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java
index 544e37e31e1..8850d49a09e 100644
--- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java
+++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java
@@ -50,7 +50,7 @@ import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.http.MediaType;
import org.springframework.http.converter.StringHttpMessageConverter;
-import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.validation.Errors;
import org.springframework.validation.ObjectError;
import org.springframework.validation.Validator;
@@ -138,7 +138,7 @@ class MethodValidationTests {
handlerAdapter.setApplicationContext(context);
handlerAdapter.setBeanFactory(context.getBeanFactory());
handlerAdapter.setMessageConverters(
- List.of(new StringHttpMessageConverter(), new MappingJackson2HttpMessageConverter()));
+ List.of(new StringHttpMessageConverter(), new JacksonJsonHttpMessageConverter()));
handlerAdapter.afterPropertiesSet();
return handlerAdapter;
}
diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java
index 1acdb9db88a..74bf7f23a22 100644
--- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java
+++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java
@@ -28,12 +28,13 @@ import java.util.List;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.annotation.JsonView;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
import org.jspecify.annotations.Nullable;
+import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.skyscreamer.jsonassert.JSONAssert;
import org.xmlunit.assertj.XmlAssert;
+import tools.jackson.databind.SerializationFeature;
+import tools.jackson.databind.json.JsonMapper;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.target.SingletonTargetSource;
@@ -51,8 +52,10 @@ import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
+import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
+import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils;
@@ -69,8 +72,6 @@ import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.support.ModelAndViewContainer;
-import org.springframework.web.servlet.ModelAndView;
-import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
import org.springframework.web.util.WebUtils;
@@ -116,7 +117,7 @@ class RequestResponseBodyMethodProcessorTests {
this.servletRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
this.servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE);
- List> converters = List.of(new MappingJackson2HttpMessageConverter());
+ List> converters = List.of(new JacksonJsonHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
@SuppressWarnings("unchecked")
@@ -151,7 +152,7 @@ class RequestResponseBodyMethodProcessorTests {
this.servletRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
this.servletRequest.setContentType("application/json");
- List> converters = List.of(new MappingJackson2HttpMessageConverter());
+ List> converters = List.of(new JacksonJsonHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
SimpleBean result = (SimpleBean) processor.resolveArgument(
@@ -207,7 +208,7 @@ class RequestResponseBodyMethodProcessorTests {
this.servletRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
this.servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE);
- List> converters = List.of(new MappingJackson2HttpMessageConverter());
+ List> converters = List.of(new JacksonJsonHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
SimpleBean result = (SimpleBean) processor.resolveArgument(methodParam, container, request, factory);
@@ -226,7 +227,7 @@ class RequestResponseBodyMethodProcessorTests {
this.servletRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
this.servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE);
- List> converters = List.of(new MappingJackson2HttpMessageConverter());
+ List> converters = List.of(new JacksonJsonHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
@SuppressWarnings("unchecked")
@@ -246,7 +247,7 @@ class RequestResponseBodyMethodProcessorTests {
this.servletRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
this.servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE);
- HttpMessageConverter target = new MappingJackson2HttpMessageConverter();
+ HttpMessageConverter target = new JacksonJsonHttpMessageConverter();
HttpMessageConverter> proxy = ProxyFactory.getProxy(HttpMessageConverter.class, new SingletonTargetSource(target));
List> converters = List.of(proxy);
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
@@ -262,7 +263,7 @@ class RequestResponseBodyMethodProcessorTests {
this.servletRequest.addHeader("Accept", "text/plain; q=0.5, application/json");
List> converters =
- List.of(new MappingJackson2HttpMessageConverter(), new StringHttpMessageConverter());
+ List.of(new JacksonJsonHttpMessageConverter(), new StringHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
processor.writeWithMessageConverters("Foo", returnTypeString, request);
@@ -331,15 +332,14 @@ class RequestResponseBodyMethodProcessorTests {
this.servletRequest.addHeader("Accept", halFormsMediaType + "," + halMediaType);
- ObjectMapper objectMapper = new ObjectMapper();
- objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true);
+ JsonMapper mapper = JsonMapper.builder().enable(SerializationFeature.INDENT_OUTPUT).build();
SimpleBean simpleBean = new SimpleBean();
simpleBean.setId(12L);
simpleBean.setName("Jason");
- MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
- converter.registerObjectMappersForType(SimpleBean.class, map -> map.put(halMediaType, objectMapper));
+ JacksonJsonHttpMessageConverter converter = new JacksonJsonHttpMessageConverter();
+ converter.registerObjectMappersForType(SimpleBean.class, map -> map.put(halMediaType, mapper));
RequestResponseBodyMethodProcessor processor =
new RequestResponseBodyMethodProcessor(List.of(converter));
MethodParameter returnType = new MethodParameter(getClass().getDeclaredMethod("getSimpleBean"), -1);
@@ -380,7 +380,7 @@ class RequestResponseBodyMethodProcessorTests {
RequestResponseBodyMethodProcessor processor =
new RequestResponseBodyMethodProcessor(List.of(
- new MappingJackson2HttpMessageConverter(), new MappingJackson2XmlHttpMessageConverter()));
+ new JacksonJsonHttpMessageConverter(), new JacksonXmlHttpMessageConverter()));
MethodParameter returnType =
new MethodParameter(getClass().getDeclaredMethod("handleAndReturnProblemDetail"), -1);
@@ -393,10 +393,10 @@ class RequestResponseBodyMethodProcessorTests {
if (expectedContentType.equals(MediaType.APPLICATION_PROBLEM_XML_VALUE)) {
XmlAssert.assertThat(this.servletResponse.getContentAsString()).and("""
- about:blank
- Bad Request
400
/path
+ Bad Request
+ about:blank
""")
.ignoreWhitespace()
.areIdentical();
@@ -413,6 +413,7 @@ class RequestResponseBodyMethodProcessorTests {
}
@Test
+ @Disabled("https://github.com/FasterXML/jackson-dataformat-xml/issues/757")
void problemDetailWhenProblemXmlRequested() throws Exception {
this.servletRequest.addHeader("Accept", MediaType.APPLICATION_PROBLEM_XML_VALUE);
testProblemDetailMediaType(MediaType.APPLICATION_PROBLEM_XML_VALUE);
@@ -504,6 +505,25 @@ class RequestResponseBodyMethodProcessorTests {
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodReturnType = handlerMethod.getReturnType();
+ List> converters = List.of(new JacksonJsonHttpMessageConverter());
+ RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(
+ converters, null, List.of(new JsonViewResponseBodyAdvice()));
+
+ Object returnValue = new JacksonController().handleResponseBody();
+ processor.handleReturnValue(returnValue, methodReturnType, this.container, this.request);
+
+ assertThat(this.servletResponse.getContentAsString())
+ .doesNotContain("\"withView1\":\"with\"")
+ .contains("\"withView2\":\"with\"")
+ .doesNotContain("\"withoutView\":\"without\"");
+ }
+
+ @Test
+ void jackson2JsonViewWithResponseBodyAndJsonMessageConverter() throws Exception {
+ Method method = JacksonController.class.getMethod("handleResponseBody");
+ HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
+ MethodParameter methodReturnType = handlerMethod.getReturnType();
+
List> converters = List.of(new MappingJackson2HttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(
converters, null, List.of(new JsonViewResponseBodyAdvice()));
@@ -523,6 +543,25 @@ class RequestResponseBodyMethodProcessorTests {
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodReturnType = handlerMethod.getReturnType();
+ List> converters = List.of(new JacksonJsonHttpMessageConverter());
+ HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(
+ converters, null, List.of(new JsonViewResponseBodyAdvice()));
+
+ Object returnValue = new JacksonController().handleResponseEntity();
+ processor.handleReturnValue(returnValue, methodReturnType, this.container, this.request);
+
+ assertThat(this.servletResponse.getContentAsString())
+ .doesNotContain("\"withView1\":\"with\"")
+ .contains("\"withView2\":\"with\"")
+ .doesNotContain("\"withoutView\":\"without\"");
+ }
+
+ @Test
+ void jackson2JsonViewWithResponseEntityAndJsonMessageConverter() throws Exception {
+ Method method = JacksonController.class.getMethod("handleResponseEntity");
+ HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
+ MethodParameter methodReturnType = handlerMethod.getReturnType();
+
List> converters = List.of(new MappingJackson2HttpMessageConverter());
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(
converters, null, List.of(new JsonViewResponseBodyAdvice()));
@@ -536,13 +575,13 @@ class RequestResponseBodyMethodProcessorTests {
.doesNotContain("\"withoutView\":\"without\"");
}
- @Test // SPR-12149
+ @Test
void jacksonJsonViewWithResponseBodyAndXmlMessageConverter() throws Exception {
Method method = JacksonController.class.getMethod("handleResponseBody");
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodReturnType = handlerMethod.getReturnType();
- List> converters = List.of(new MappingJackson2XmlHttpMessageConverter());
+ List> converters = List.of(new JacksonXmlHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(
converters, null, List.of(new JsonViewResponseBodyAdvice()));
@@ -556,11 +595,49 @@ class RequestResponseBodyMethodProcessorTests {
}
@Test // SPR-12149
+ void jackson2JsonViewWithResponseBodyAndXmlMessageConverter() throws Exception {
+ Method method = JacksonController.class.getMethod("handleResponseBody");
+ HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
+ MethodParameter methodReturnType = handlerMethod.getReturnType();
+
+ List> converters = List.of(new MappingJackson2XmlHttpMessageConverter());
+ RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(
+ converters, null, List.of(new JsonViewResponseBodyAdvice()));
+
+ Object returnValue = new JacksonController().handleResponseBody();
+ processor.handleReturnValue(returnValue, methodReturnType, this.container, this.request);
+
+ assertThat(this.servletResponse.getContentAsString())
+ .doesNotContain("with ")
+ .contains("with ")
+ .doesNotContain("without ");
+ }
+
+ @Test
void jacksonJsonViewWithResponseEntityAndXmlMessageConverter() throws Exception {
Method method = JacksonController.class.getMethod("handleResponseEntity");
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodReturnType = handlerMethod.getReturnType();
+ List> converters = List.of(new JacksonXmlHttpMessageConverter());
+ HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(
+ converters, null, List.of(new JsonViewResponseBodyAdvice()));
+
+ Object returnValue = new JacksonController().handleResponseEntity();
+ processor.handleReturnValue(returnValue, methodReturnType, this.container, this.request);
+
+ assertThat(this.servletResponse.getContentAsString())
+ .doesNotContain("with ")
+ .contains("with ")
+ .doesNotContain("without ");
+ }
+
+ @Test // SPR-12149
+ void jackson2JsonViewWithResponseEntityAndXmlMessageConverter() throws Exception {
+ Method method = JacksonController.class.getMethod("handleResponseEntity");
+ HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
+ MethodParameter methodReturnType = handlerMethod.getReturnType();
+
List> converters = List.of(new MappingJackson2XmlHttpMessageConverter());
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(
converters, null, List.of(new JsonViewResponseBodyAdvice()));
@@ -574,7 +651,7 @@ class RequestResponseBodyMethodProcessorTests {
.doesNotContain("without ");
}
- @Test // SPR-12501
+ @Test
void resolveArgumentWithJacksonJsonView() throws Exception {
String content = "{\"withView1\" : \"with\", \"withView2\" : \"with\", \"withoutView\" : \"without\"}";
this.servletRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
@@ -584,7 +661,7 @@ class RequestResponseBodyMethodProcessorTests {
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodParameter = handlerMethod.getMethodParameters()[0];
- List> converters = List.of(new MappingJackson2HttpMessageConverter());
+ List> converters = List.of(new JacksonJsonHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(
converters, null, List.of(new JsonViewRequestBodyAdvice()));
@@ -598,6 +675,29 @@ class RequestResponseBodyMethodProcessorTests {
}
@Test // SPR-12501
+ void resolveArgumentWithJackson2JsonView() throws Exception {
+ String content = "{\"withView1\" : \"with\", \"withView2\" : \"with\", \"withoutView\" : \"without\"}";
+ this.servletRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
+ this.servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE);
+
+ Method method = JacksonController.class.getMethod("handleRequestBody", JacksonViewBean.class);
+ HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
+ MethodParameter methodParameter = handlerMethod.getMethodParameters()[0];
+
+ List> converters = List.of(new MappingJackson2HttpMessageConverter());
+ RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(
+ converters, null, List.of(new JsonViewRequestBodyAdvice()));
+
+ JacksonViewBean result = (JacksonViewBean)
+ processor.resolveArgument(methodParameter, this.container, this.request, this.factory);
+
+ assertThat(result).isNotNull();
+ assertThat(result.getWithView1()).isEqualTo("with");
+ assertThat(result.getWithView2()).isNull();
+ assertThat(result.getWithoutView()).isNull();
+ }
+
+ @Test
void resolveHttpEntityArgumentWithJacksonJsonView() throws Exception {
String content = "{\"withView1\" : \"with\", \"withView2\" : \"with\", \"withoutView\" : \"without\"}";
this.servletRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
@@ -607,7 +707,7 @@ class RequestResponseBodyMethodProcessorTests {
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodParameter = handlerMethod.getMethodParameters()[0];
- List> converters = List.of(new MappingJackson2HttpMessageConverter());
+ List> converters = List.of(new JacksonJsonHttpMessageConverter());
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(
converters, null, List.of(new JsonViewRequestBodyAdvice()));
@@ -623,6 +723,31 @@ class RequestResponseBodyMethodProcessorTests {
}
@Test // SPR-12501
+ void resolveHttpEntityArgumentWithJackson2JsonView() throws Exception {
+ String content = "{\"withView1\" : \"with\", \"withView2\" : \"with\", \"withoutView\" : \"without\"}";
+ this.servletRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
+ this.servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE);
+
+ Method method = JacksonController.class.getMethod("handleHttpEntity", HttpEntity.class);
+ HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
+ MethodParameter methodParameter = handlerMethod.getMethodParameters()[0];
+
+ List> converters = List.of(new MappingJackson2HttpMessageConverter());
+ HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(
+ converters, null, List.of(new JsonViewRequestBodyAdvice()));
+
+ @SuppressWarnings("unchecked")
+ HttpEntity result = (HttpEntity)
+ processor.resolveArgument( methodParameter, this.container, this.request, this.factory);
+
+ assertThat(result).isNotNull();
+ assertThat(result.getBody()).isNotNull();
+ assertThat(result.getBody().getWithView1()).isEqualTo("with");
+ assertThat(result.getBody().getWithView2()).isNull();
+ assertThat(result.getBody().getWithoutView()).isNull();
+ }
+
+ @Test
void resolveArgumentWithJacksonJsonViewAndXmlMessageConverter() throws Exception {
String content = "" +
"with " +
@@ -635,7 +760,7 @@ class RequestResponseBodyMethodProcessorTests {
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodParameter = handlerMethod.getMethodParameters()[0];
- List> converters = List.of(new MappingJackson2XmlHttpMessageConverter());
+ List> converters = List.of(new JacksonXmlHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(
converters, null, List.of(new JsonViewRequestBodyAdvice()));
@@ -649,6 +774,32 @@ class RequestResponseBodyMethodProcessorTests {
}
@Test // SPR-12501
+ void resolveArgumentWithJackson2JsonViewAndXmlMessageConverter() throws Exception {
+ String content = "" +
+ "with " +
+ "with " +
+ "without ";
+ this.servletRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
+ this.servletRequest.setContentType(MediaType.APPLICATION_XML_VALUE);
+
+ Method method = JacksonController.class.getMethod("handleRequestBody", JacksonViewBean.class);
+ HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
+ MethodParameter methodParameter = handlerMethod.getMethodParameters()[0];
+
+ List> converters = List.of(new MappingJackson2XmlHttpMessageConverter());
+ RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(
+ converters, null, List.of(new JsonViewRequestBodyAdvice()));
+
+ JacksonViewBean result = (JacksonViewBean)
+ processor.resolveArgument(methodParameter, this.container, this.request, this.factory);
+
+ assertThat(result).isNotNull();
+ assertThat(result.getWithView1()).isEqualTo("with");
+ assertThat(result.getWithView2()).isNull();
+ assertThat(result.getWithoutView()).isNull();
+ }
+
+ @Test
void resolveHttpEntityArgumentWithJacksonJsonViewAndXmlMessageConverter() throws Exception {
String content = "" +
"with " +
@@ -661,6 +812,34 @@ class RequestResponseBodyMethodProcessorTests {
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodParameter = handlerMethod.getMethodParameters()[0];
+ List> converters = List.of(new JacksonXmlHttpMessageConverter());
+ HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(
+ converters, null, List.of(new JsonViewRequestBodyAdvice()));
+
+ @SuppressWarnings("unchecked")
+ HttpEntity result = (HttpEntity)
+ processor.resolveArgument(methodParameter, this.container, this.request, this.factory);
+
+ assertThat(result).isNotNull();
+ assertThat(result.getBody()).isNotNull();
+ assertThat(result.getBody().getWithView1()).isEqualTo("with");
+ assertThat(result.getBody().getWithView2()).isNull();
+ assertThat(result.getBody().getWithoutView()).isNull();
+ }
+
+ @Test // SPR-12501
+ void resolveHttpEntityArgumentWithJackson2JsonViewAndXmlMessageConverter() throws Exception {
+ String content = "" +
+ "with " +
+ "with " +
+ "without ";
+ this.servletRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
+ this.servletRequest.setContentType(MediaType.APPLICATION_XML_VALUE);
+
+ Method method = JacksonController.class.getMethod("handleHttpEntity", HttpEntity.class);
+ HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
+ MethodParameter methodParameter = handlerMethod.getMethodParameters()[0];
+
List> converters = List.of(new MappingJackson2XmlHttpMessageConverter());
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(
converters, null, List.of(new JsonViewRequestBodyAdvice()));
@@ -676,12 +855,29 @@ class RequestResponseBodyMethodProcessorTests {
assertThat(result.getBody().getWithoutView()).isNull();
}
- @Test // SPR-12811
+ @Test
void jacksonTypeInfoList() throws Exception {
Method method = JacksonController.class.getMethod("handleTypeInfoList");
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodReturnType = handlerMethod.getReturnType();
+ List> converters = List.of(new JacksonJsonHttpMessageConverter());
+ RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
+
+ Object returnValue = new JacksonController().handleTypeInfoList();
+ processor.handleReturnValue(returnValue, methodReturnType, this.container, this.request);
+
+ assertThat(this.servletResponse.getContentAsString())
+ .contains("\"type\":\"foo\"")
+ .contains("\"type\":\"bar\"");
+ }
+
+ @Test // SPR-12811
+ void jackson2TypeInfoList() throws Exception {
+ Method method = JacksonController.class.getMethod("handleTypeInfoList");
+ HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
+ MethodParameter methodReturnType = handlerMethod.getReturnType();
+
List> converters = List.of(new MappingJackson2HttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
@@ -693,13 +889,13 @@ class RequestResponseBodyMethodProcessorTests {
.contains("\"type\":\"bar\"");
}
- @Test // SPR-13318
+ @Test
void jacksonSubType() throws Exception {
Method method = JacksonController.class.getMethod("handleSubType");
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodReturnType = handlerMethod.getReturnType();
- List> converters = List.of(new MappingJackson2HttpMessageConverter());
+ List> converters = List.of(new JacksonJsonHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
Object returnValue = new JacksonController().handleSubType();
@@ -711,11 +907,47 @@ class RequestResponseBodyMethodProcessorTests {
}
@Test // SPR-13318
+ void jackson2SubType() throws Exception {
+ Method method = JacksonController.class.getMethod("handleSubType");
+ HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
+ MethodParameter methodReturnType = handlerMethod.getReturnType();
+
+ List> converters = List.of(new MappingJackson2HttpMessageConverter());
+ RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
+
+ Object returnValue = new JacksonController().handleSubType();
+ processor.handleReturnValue(returnValue, methodReturnType, this.container, this.request);
+
+ assertThat(this.servletResponse.getContentAsString())
+ .contains("\"id\":123")
+ .contains("\"name\":\"foo\"");
+ }
+
+ @Test
void jacksonSubTypeList() throws Exception {
Method method = JacksonController.class.getMethod("handleSubTypeList");
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodReturnType = handlerMethod.getReturnType();
+ List> converters = List.of(new JacksonJsonHttpMessageConverter());
+ RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
+
+ Object returnValue = new JacksonController().handleSubTypeList();
+ processor.handleReturnValue(returnValue, methodReturnType, this.container, this.request);
+
+ assertThat(this.servletResponse.getContentAsString())
+ .contains("\"id\":123")
+ .contains("\"name\":\"foo\"")
+ .contains("\"id\":456")
+ .contains("\"name\":\"bar\"");
+ }
+
+ @Test // SPR-13318
+ void jackson2SubTypeList() throws Exception {
+ Method method = JacksonController.class.getMethod("handleSubTypeList");
+ HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
+ MethodParameter methodReturnType = handlerMethod.getReturnType();
+
List> converters = List.of(new MappingJackson2HttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
@@ -738,7 +970,7 @@ class RequestResponseBodyMethodProcessorTests {
HandlerMethod handlerMethod = new HandlerMethod(new MyControllerImplementingInterface(), method);
MethodParameter methodParameter = handlerMethod.getMethodParameters()[0];
- List> converters = List.of(new MappingJackson2HttpMessageConverter());
+ List> converters = List.of(new JacksonJsonHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
assertThat(processor.supportsParameter(methodParameter)).isTrue();
@@ -756,7 +988,7 @@ class RequestResponseBodyMethodProcessorTests {
HandlerMethod handlerMethod = new HandlerMethod(new SubControllerImplementingInterface(), method);
MethodParameter methodParameter = handlerMethod.getMethodParameters()[0];
- List> converters = List.of(new MappingJackson2HttpMessageConverter());
+ List> converters = List.of(new JacksonJsonHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
assertThat(processor.supportsParameter(methodParameter)).isTrue();
@@ -774,7 +1006,7 @@ class RequestResponseBodyMethodProcessorTests {
HandlerMethod handlerMethod = new HandlerMethod(new SubControllerImplementingAbstractMethod(), method);
MethodParameter methodParameter = handlerMethod.getMethodParameters()[0];
- List> converters = List.of(new MappingJackson2HttpMessageConverter());
+ List> converters = List.of(new JacksonJsonHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
assertThat(processor.supportsParameter(methodParameter)).isTrue();
@@ -1068,8 +1300,6 @@ class RequestResponseBodyMethodProcessorTests {
bean.setWithView1("with");
bean.setWithView2("with");
bean.setWithoutView("without");
- ModelAndView mav = new ModelAndView(new MappingJackson2JsonView());
- mav.addObject("bean", bean);
return new ResponseEntity<>(bean, HttpStatus.OK);
}
diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandlerTests.java
index 1d7e1565e7c..158719595b7 100644
--- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandlerTests.java
+++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandlerTests.java
@@ -35,7 +35,7 @@ import reactor.core.scheduler.Schedulers;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.http.ResponseEntity;
-import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.async.AsyncWebRequest;
@@ -63,7 +63,7 @@ import static org.springframework.web.testfixture.method.ResolvableMethod.on;
class ResponseBodyEmitterReturnValueHandlerTests {
private final ResponseBodyEmitterReturnValueHandler handler =
- new ResponseBodyEmitterReturnValueHandler(List.of(new MappingJackson2HttpMessageConverter()));
+ new ResponseBodyEmitterReturnValueHandler(List.of(new JacksonJsonHttpMessageConverter()));
private final MockHttpServletRequest request = new MockHttpServletRequest();
diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethodTests.java
index 03ac4e60fff..8620e1aff30 100644
--- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethodTests.java
+++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethodTests.java
@@ -41,7 +41,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.StringHttpMessageConverter;
-import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@@ -373,7 +373,7 @@ class ServletInvocableHandlerMethodTests {
@Test
void wrapConcurrentResult_CollectedValuesList() throws Exception {
- List> converters = Collections.singletonList(new MappingJackson2HttpMessageConverter());
+ List> converters = Collections.singletonList(new JacksonJsonHttpMessageConverter());
ResolvableType elementType = ResolvableType.forClass(List.class);
ReactiveTypeHandler.CollectedValuesList result = new ReactiveTypeHandler.CollectedValuesList(elementType);
result.add(Arrays.asList("foo1", "bar1"));
@@ -391,7 +391,7 @@ class ServletInvocableHandlerMethodTests {
@Test // SPR-15478
public void wrapConcurrentResult_CollectedValuesListWithResponseEntity() throws Exception {
- List> converters = Collections.singletonList(new MappingJackson2HttpMessageConverter());
+ List> converters = Collections.singletonList(new JacksonJsonHttpMessageConverter());
ResolvableType elementType = ResolvableType.forClass(Bar.class);
ReactiveTypeHandler.CollectedValuesList result = new ReactiveTypeHandler.CollectedValuesList(elementType);
result.add(new Bar("foo"));