Browse Source

Introduce Jackson 3 support for converters

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

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

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

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

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

JacksonHandlerInstantiator needs to be enabled explicitly if needed.

See gh-33798
pull/34893/head
Sébastien Deleuze 7 months ago
parent
commit
d4e4a9ae06
  1. 509
      spring-web/src/main/java/org/springframework/http/converter/AbstractJacksonHttpMessageConverter.java
  2. 61
      spring-web/src/main/java/org/springframework/http/converter/cbor/JacksonCborHttpMessageConverter.java
  3. 124
      spring-web/src/main/java/org/springframework/http/converter/json/JacksonJsonHttpMessageConverter.java
  4. 63
      spring-web/src/main/java/org/springframework/http/converter/smile/JacksonSmileHttpMessageConverter.java
  5. 55
      spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java
  6. 111
      spring-web/src/main/java/org/springframework/http/converter/xml/JacksonXmlHttpMessageConverter.java
  7. 61
      spring-web/src/main/java/org/springframework/http/converter/yaml/JacksonYamlHttpMessageConverter.java
  8. 51
      spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java
  9. 53
      spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
  10. 157
      spring-web/src/test/java/org/springframework/http/converter/cbor/JacksonCborHttpMessageConverterTests.java
  11. 843
      spring-web/src/test/java/org/springframework/http/converter/json/JacksonJsonHttpMessageConverterTests.java
  12. 157
      spring-web/src/test/java/org/springframework/http/converter/smile/JacksonSmileHttpMessageConverterTests.java
  13. 319
      spring-web/src/test/java/org/springframework/http/converter/xml/JacksonXmlHttpMessageConverterTests.java
  14. 157
      spring-web/src/test/java/org/springframework/http/converter/yaml/JacksonYamlHttpMessageConverterTests.java
  15. 2
      spring-web/src/test/java/org/springframework/web/client/RestClientIntegrationTests.java
  16. 6
      spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java
  17. 4
      spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java
  18. 6
      spring-webmvc/spring-webmvc.gradle
  19. 69
      spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java
  20. 4
      spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java
  21. 292
      spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java
  22. 4
      spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandlerTests.java
  23. 6
      spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethodTests.java

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

@ -0,0 +1,509 @@ @@ -0,0 +1,509 @@
/*
* 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;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import com.fasterxml.jackson.annotation.JsonView;
import org.jspecify.annotations.Nullable;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonEncoding;
import tools.jackson.core.JsonGenerator;
import tools.jackson.core.PrettyPrinter;
import tools.jackson.core.util.DefaultIndenter;
import tools.jackson.core.util.DefaultPrettyPrinter;
import tools.jackson.databind.JacksonModule;
import tools.jackson.databind.JavaType;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectReader;
import tools.jackson.databind.ObjectWriter;
import tools.jackson.databind.SerializationConfig;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.cfg.MapperBuilder;
import tools.jackson.databind.exc.InvalidDefinitionException;
import tools.jackson.databind.ser.FilterProvider;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.ProblemDetail;
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonInputMessage;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StreamUtils;
import org.springframework.util.TypeUtils;
/**
* Abstract base class for Jackson based and content type independent
* {@link HttpMessageConverter} implementations.
*
* <p>The following hint entries are supported:
* <ul>
* <li>A JSON view with a <code>com.fasterxml.jackson.annotation.JsonView</code>
* key and the class name of the JSON view as value.</li>
* <li>A filter provider with a <code>tools.jackson.databind.ser.FilterProvider</code>
* key and the filter provider class name as value.</li>
* </ul>
*
* @author Sebastien Deleuze
* @since 7.0
* @see JacksonJsonHttpMessageConverter
*/
public abstract class AbstractJacksonHttpMessageConverter extends AbstractSmartHttpMessageConverter<Object> {
private static final String JSON_VIEW_HINT = JsonView.class.getName();
private static final String FILTER_PROVIDER_HINT = FilterProvider.class.getName();
private static final Map<String, JsonEncoding> ENCODINGS;
private static volatile @Nullable List<JacksonModule> modules = null;
static {
ENCODINGS = CollectionUtils.newHashMap(JsonEncoding.values().length);
for (JsonEncoding encoding : JsonEncoding.values()) {
ENCODINGS.put(encoding.getJavaName(), encoding);
}
ENCODINGS.put("US-ASCII", JsonEncoding.UTF8);
}
protected final ObjectMapper defaultObjectMapper;
private @Nullable Map<Class<?>, Map<MediaType, ObjectMapper>> objectMapperRegistrations;
private final @Nullable PrettyPrinter ssePrettyPrinter;
/**
* Construct a new instance with a provided {@link MapperBuilder builder}
* customized with the {@link tools.jackson.databind.JacksonModule}s found
* by {@link MapperBuilder#findModules(ClassLoader)}.
*/
private AbstractJacksonHttpMessageConverter(MapperBuilder<?, ?> builder) {
this.defaultObjectMapper = builder.addModules(initModules()).build();
this.ssePrettyPrinter = initSsePrettyPrinter();
}
/**
* Construct a new instance with the provided {@link MapperBuilder builder}
* customized with the {@link tools.jackson.databind.JacksonModule}s found
* by {@link MapperBuilder#findModules(ClassLoader)} and {@link MediaType}.
*/
protected AbstractJacksonHttpMessageConverter(MapperBuilder<?, ?> builder, MediaType supportedMediaType) {
this(builder);
setSupportedMediaTypes(Collections.singletonList(supportedMediaType));
}
/**
* Construct a new instance with the provided {@link MapperBuilder builder}
* customized with the {@link tools.jackson.databind.JacksonModule}s found
* by {@link MapperBuilder#findModules(ClassLoader)} and {@link MediaType}s.
*/
protected AbstractJacksonHttpMessageConverter(MapperBuilder<?, ?> builder, MediaType... supportedMediaTypes) {
this(builder);
setSupportedMediaTypes(Arrays.asList(supportedMediaTypes));
}
/**
* Construct a new instance with a provided {@link ObjectMapper}.
*/
protected AbstractJacksonHttpMessageConverter(ObjectMapper objectMapper) {
this.defaultObjectMapper = objectMapper;
this.ssePrettyPrinter = initSsePrettyPrinter();
}
/**
* Construct a new instance with the provided {@link ObjectMapper} and {@link MediaType}.
*/
protected AbstractJacksonHttpMessageConverter(ObjectMapper objectMapper, MediaType supportedMediaType) {
this(objectMapper);
setSupportedMediaTypes(Collections.singletonList(supportedMediaType));
}
/**
* Construct a new instance with the provided {@link ObjectMapper} and {@link MediaType}s.
*/
protected AbstractJacksonHttpMessageConverter(ObjectMapper objectMapper, MediaType... supportedMediaTypes) {
this(objectMapper);
setSupportedMediaTypes(Arrays.asList(supportedMediaTypes));
}
private List<JacksonModule> initModules() {
if (modules == null) {
modules = MapperBuilder.findModules(AbstractJacksonHttpMessageConverter.class.getClassLoader());
}
return Objects.requireNonNull(modules);
}
private PrettyPrinter initSsePrettyPrinter() {
DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter();
prettyPrinter.indentObjectsWith(new DefaultIndenter(" ", "\ndata:"));
return prettyPrinter;
}
@Override
public void setSupportedMediaTypes(List<MediaType> 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}.
* <p><strong>Note:</strong> 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<Map<MediaType, ObjectMapper>> registrar) {
if (this.objectMapperRegistrations == null) {
this.objectMapperRegistrations = new LinkedHashMap<>();
}
Map<MediaType, ObjectMapper> 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<MediaType, ObjectMapper> getObjectMappersForType(Class<?> clazz) {
for (Map.Entry<Class<?>, Map<MediaType, ObjectMapper>> entry : getObjectMapperRegistrations().entrySet()) {
if (entry.getKey().isAssignableFrom(clazz)) {
return entry.getValue();
}
}
return Collections.emptyMap();
}
@Override
public List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
List<MediaType> result = null;
for (Map.Entry<Class<?>, Map<MediaType, ObjectMapper>> 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<Class<?>, Map<MediaType, ObjectMapper>> 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<MediaType> 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<Class<?>, Map<MediaType, ObjectMapper>> typeEntry : getObjectMapperRegistrations().entrySet()) {
if (typeEntry.getKey().isAssignableFrom(targetType)) {
for (Map.Entry<MediaType, ObjectMapper> 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<String, Object> 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<String, Object> 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.
* <p>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<String, Object> 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;
}
}

61
spring-web/src/main/java/org/springframework/http/converter/cbor/JacksonCborHttpMessageConverter.java

@ -0,0 +1,61 @@ @@ -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 <a href="https://cbor.io/">CBOR</a>
* data format using <a href="https://github.com/FasterXML/jackson-dataformats-binary/tree/3.x/cbor">
* the dedicated Jackson 3.x extension</a>.
*
* <p>By default, this converter supports the {@link MediaType#APPLICATION_CBOR_VALUE}
* media type. This can be overridden by setting the {@link #setSupportedMediaTypes
* supportedMediaTypes} property.
*
* <p>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);
}
}

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

@ -0,0 +1,124 @@ @@ -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 <a href="https://github.com/FasterXML/jackson">Jackson 3.x's</a>
* {@link ObjectMapper}.
*
* <p>This converter can be used to bind to typed beans, or untyped
* {@code HashMap} instances.
*
* <p>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.
*
* <p>The default constructor loads {@link tools.jackson.databind.JacksonModule}s
* found by {@link MapperBuilder#findModules(ClassLoader)}.
*
* <p>The following hints entries are supported:
* <ul>
* <li>A JSON view with a <code>com.fasterxml.jackson.annotation.JsonView</code>
* key and the class name of the JSON view as value.</li>
* <li>A filter provider with a <code>tools.jackson.databind.ser.FilterProvider</code>
* key and the filter provider class name as value.</li>
* </ul>
*
* @author Sebastien Deleuze
* @since 7.0
*/
public class JacksonJsonHttpMessageConverter extends AbstractJacksonHttpMessageConverter {
private static final List<MediaType> 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}.
* <p>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<MediaType> getMediaTypesForProblemDetail() {
return problemDetailMediaTypes;
}
@Override
protected void writePrefix(JsonGenerator generator, Object object) {
if (this.jsonPrefix != null) {
generator.writeRaw(this.jsonPrefix);
}
}
}

63
spring-web/src/main/java/org/springframework/http/converter/smile/JacksonSmileHttpMessageConverter.java

@ -0,0 +1,63 @@ @@ -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
* <a href="https://github.com/FasterXML/jackson-dataformats-binary/tree/3.x/smile">
* the dedicated Jackson 3.x extension</a>.
*
* <p>By default, this converter supports {@code "application/x-jackson-smile"}
* media type. This can be overridden by setting the
* {@link #setSupportedMediaTypes supportedMediaTypes} property.
*
* <p>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);
}
}

55
spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java

@ -17,16 +17,21 @@ @@ -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 @@ -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 @@ -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 @@ -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 @@ -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());
}

111
spring-web/src/main/java/org/springframework/http/converter/xml/JacksonXmlHttpMessageConverter.java

@ -0,0 +1,111 @@ @@ -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 <a href="https://github.com/FasterXML/jackson-dataformat-xml">
* Jackson 3.x extension component for reading and writing XML encoded data</a>.
*
* <p>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.
*
* <p>The default constructor loads {@link tools.jackson.databind.JacksonModule}s
* found by {@link MapperBuilder#findModules(ClassLoader)}.
*
* <p>The following hint entries are supported:
* <ul>
* <li>A JSON view with a <code>com.fasterxml.jackson.annotation.JsonView</code>
* key and the class name of the JSON view as value.</li>
* <li>A filter provider with a <code>tools.jackson.databind.ser.FilterProvider</code>
* key and the filter provider class name as value.</li>
* </ul>
*
* @author Sebastien Deleuze
* @since 7.0
*/
public class JacksonXmlHttpMessageConverter extends AbstractJacksonHttpMessageConverter {
private static final List<MediaType> 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<MediaType> getMediaTypesForProblemDetail() {
return problemDetailMediaTypes;
}
}

61
spring-web/src/main/java/org/springframework/http/converter/yaml/JacksonYamlHttpMessageConverter.java

@ -0,0 +1,61 @@ @@ -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 <a href="https://yaml.io/">YAML</a>
* data format using <a href="https://github.com/FasterXML/jackson-dataformats-text/tree/3.x/yaml">
* the dedicated Jackson 3.x extension</a>.
*
* <p>By default, this converter supports the {@link MediaType#APPLICATION_YAML_VALUE}
* media type. This can be overridden by setting the {@link #setSupportedMediaTypes
* supportedMediaTypes} property.
*
* <p>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);
}
}

51
spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java

@ -47,19 +47,24 @@ import org.springframework.http.converter.ByteArrayHttpMessageConverter; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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());
}
}

53
spring-web/src/main/java/org/springframework/web/client/RestTemplate.java

@ -56,19 +56,24 @@ import org.springframework.http.converter.HttpMessageConverter; @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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());
}

157
spring-web/src/test/java/org/springframework/http/converter/cbor/JacksonCborHttpMessageConverterTests.java

@ -0,0 +1,157 @@ @@ -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;
}
}
}

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

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

157
spring-web/src/test/java/org/springframework/http/converter/smile/JacksonSmileHttpMessageConverterTests.java

@ -0,0 +1,157 @@ @@ -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;
}
}
}

319
spring-web/src/test/java/org/springframework/http/converter/xml/JacksonXmlHttpMessageConverterTests.java

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

157
spring-web/src/test/java/org/springframework/http/converter/yaml/JacksonYamlHttpMessageConverterTests.java

@ -0,0 +1,157 @@ @@ -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;
}
}
}

2
spring-web/src/test/java/org/springframework/web/client/RestClientIntegrationTests.java

@ -522,7 +522,7 @@ class RestClientIntegrationTests { @@ -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");
});

6
spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java

@ -1,5 +1,5 @@ @@ -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; @@ -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 { @@ -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();

4
spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java

@ -53,8 +53,8 @@ import org.springframework.http.converter.GenericHttpMessageConverter; @@ -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 { @@ -113,7 +113,7 @@ class RestTemplateTests {
RestTemplate restTemplate = new RestTemplate();
List<HttpMessageConverter<?>> httpMessageConverters = restTemplate.getMessageConverters();
assertThat(httpMessageConverters).extracting("class").containsOnlyOnce(
MappingJackson2HttpMessageConverter.class
JacksonJsonHttpMessageConverter.class
);
assertThat(httpMessageConverters).extracting("class").doesNotContain(
KotlinSerializationJsonHttpMessageConverter.class

6
spring-webmvc/spring-webmvc.gradle

@ -39,6 +39,12 @@ dependencies { @@ -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"
}

69
spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java

@ -45,20 +45,25 @@ import org.springframework.http.converter.HttpMessageConverter; @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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()));
}

4
spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java

@ -50,7 +50,7 @@ import org.springframework.context.MessageSourceResolvable; @@ -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 { @@ -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;
}

292
spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java

@ -28,12 +28,13 @@ import java.util.List; @@ -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; @@ -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; @@ -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 { @@ -116,7 +117,7 @@ class RequestResponseBodyMethodProcessorTests {
this.servletRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
this.servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE);
List<HttpMessageConverter<?>> converters = List.of(new MappingJackson2HttpMessageConverter());
List<HttpMessageConverter<?>> converters = List.of(new JacksonJsonHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
@SuppressWarnings("unchecked")
@ -151,7 +152,7 @@ class RequestResponseBodyMethodProcessorTests { @@ -151,7 +152,7 @@ class RequestResponseBodyMethodProcessorTests {
this.servletRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
this.servletRequest.setContentType("application/json");
List<HttpMessageConverter<?>> converters = List.of(new MappingJackson2HttpMessageConverter());
List<HttpMessageConverter<?>> converters = List.of(new JacksonJsonHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
SimpleBean result = (SimpleBean) processor.resolveArgument(
@ -207,7 +208,7 @@ class RequestResponseBodyMethodProcessorTests { @@ -207,7 +208,7 @@ class RequestResponseBodyMethodProcessorTests {
this.servletRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
this.servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE);
List<HttpMessageConverter<?>> converters = List.of(new MappingJackson2HttpMessageConverter());
List<HttpMessageConverter<?>> 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 { @@ -226,7 +227,7 @@ class RequestResponseBodyMethodProcessorTests {
this.servletRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
this.servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE);
List<HttpMessageConverter<?>> converters = List.of(new MappingJackson2HttpMessageConverter());
List<HttpMessageConverter<?>> converters = List.of(new JacksonJsonHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
@SuppressWarnings("unchecked")
@ -246,7 +247,7 @@ class RequestResponseBodyMethodProcessorTests { @@ -246,7 +247,7 @@ class RequestResponseBodyMethodProcessorTests {
this.servletRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
this.servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE);
HttpMessageConverter<Object> target = new MappingJackson2HttpMessageConverter();
HttpMessageConverter<Object> target = new JacksonJsonHttpMessageConverter();
HttpMessageConverter<?> proxy = ProxyFactory.getProxy(HttpMessageConverter.class, new SingletonTargetSource(target));
List<HttpMessageConverter<?>> converters = List.of(proxy);
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
@ -262,7 +263,7 @@ class RequestResponseBodyMethodProcessorTests { @@ -262,7 +263,7 @@ class RequestResponseBodyMethodProcessorTests {
this.servletRequest.addHeader("Accept", "text/plain; q=0.5, application/json");
List<HttpMessageConverter<?>> 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 { @@ -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 { @@ -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 { @@ -393,10 +393,10 @@ class RequestResponseBodyMethodProcessorTests {
if (expectedContentType.equals(MediaType.APPLICATION_PROBLEM_XML_VALUE)) {
XmlAssert.assertThat(this.servletResponse.getContentAsString()).and("""
<problem xmlns="urn:ietf:rfc:7807">
<type>about:blank</type>
<title>Bad Request</title>
<status>400</status>
<instance>/path</instance>
<title>Bad Request</title>
<type>about:blank</type>
</problem>""")
.ignoreWhitespace()
.areIdentical();
@ -413,6 +413,7 @@ class RequestResponseBodyMethodProcessorTests { @@ -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 { @@ -504,6 +505,25 @@ class RequestResponseBodyMethodProcessorTests {
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodReturnType = handlerMethod.getReturnType();
List<HttpMessageConverter<?>> 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<HttpMessageConverter<?>> converters = List.of(new MappingJackson2HttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(
converters, null, List.of(new JsonViewResponseBodyAdvice()));
@ -523,6 +543,25 @@ class RequestResponseBodyMethodProcessorTests { @@ -523,6 +543,25 @@ class RequestResponseBodyMethodProcessorTests {
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodReturnType = handlerMethod.getReturnType();
List<HttpMessageConverter<?>> 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<HttpMessageConverter<?>> converters = List.of(new MappingJackson2HttpMessageConverter());
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(
converters, null, List.of(new JsonViewResponseBodyAdvice()));
@ -536,13 +575,13 @@ class RequestResponseBodyMethodProcessorTests { @@ -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<HttpMessageConverter<?>> converters = List.of(new MappingJackson2XmlHttpMessageConverter());
List<HttpMessageConverter<?>> converters = List.of(new JacksonXmlHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(
converters, null, List.of(new JsonViewResponseBodyAdvice()));
@ -556,11 +595,49 @@ class RequestResponseBodyMethodProcessorTests { @@ -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<HttpMessageConverter<?>> 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("<withView1>with</withView1>")
.contains("<withView2>with</withView2>")
.doesNotContain("<withoutView>without</withoutView>");
}
@Test
void jacksonJsonViewWithResponseEntityAndXmlMessageConverter() throws Exception {
Method method = JacksonController.class.getMethod("handleResponseEntity");
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodReturnType = handlerMethod.getReturnType();
List<HttpMessageConverter<?>> 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("<withView1>with</withView1>")
.contains("<withView2>with</withView2>")
.doesNotContain("<withoutView>without</withoutView>");
}
@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<HttpMessageConverter<?>> converters = List.of(new MappingJackson2XmlHttpMessageConverter());
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(
converters, null, List.of(new JsonViewResponseBodyAdvice()));
@ -574,7 +651,7 @@ class RequestResponseBodyMethodProcessorTests { @@ -574,7 +651,7 @@ class RequestResponseBodyMethodProcessorTests {
.doesNotContain("<withoutView>without</withoutView>");
}
@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 { @@ -584,7 +661,7 @@ class RequestResponseBodyMethodProcessorTests {
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodParameter = handlerMethod.getMethodParameters()[0];
List<HttpMessageConverter<?>> converters = List.of(new MappingJackson2HttpMessageConverter());
List<HttpMessageConverter<?>> converters = List.of(new JacksonJsonHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(
converters, null, List.of(new JsonViewRequestBodyAdvice()));
@ -598,6 +675,29 @@ class RequestResponseBodyMethodProcessorTests { @@ -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<HttpMessageConverter<?>> 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 { @@ -607,7 +707,7 @@ class RequestResponseBodyMethodProcessorTests {
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodParameter = handlerMethod.getMethodParameters()[0];
List<HttpMessageConverter<?>> converters = List.of(new MappingJackson2HttpMessageConverter());
List<HttpMessageConverter<?>> converters = List.of(new JacksonJsonHttpMessageConverter());
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(
converters, null, List.of(new JsonViewRequestBodyAdvice()));
@ -623,6 +723,31 @@ class RequestResponseBodyMethodProcessorTests { @@ -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<HttpMessageConverter<?>> converters = List.of(new MappingJackson2HttpMessageConverter());
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(
converters, null, List.of(new JsonViewRequestBodyAdvice()));
@SuppressWarnings("unchecked")
HttpEntity<JacksonViewBean> result = (HttpEntity<JacksonViewBean>)
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 = "<root>" +
"<withView1>with</withView1>" +
@ -635,7 +760,7 @@ class RequestResponseBodyMethodProcessorTests { @@ -635,7 +760,7 @@ class RequestResponseBodyMethodProcessorTests {
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodParameter = handlerMethod.getMethodParameters()[0];
List<HttpMessageConverter<?>> converters = List.of(new MappingJackson2XmlHttpMessageConverter());
List<HttpMessageConverter<?>> converters = List.of(new JacksonXmlHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(
converters, null, List.of(new JsonViewRequestBodyAdvice()));
@ -649,6 +774,32 @@ class RequestResponseBodyMethodProcessorTests { @@ -649,6 +774,32 @@ class RequestResponseBodyMethodProcessorTests {
}
@Test // SPR-12501
void resolveArgumentWithJackson2JsonViewAndXmlMessageConverter() throws Exception {
String content = "<root>" +
"<withView1>with</withView1>" +
"<withView2>with</withView2>" +
"<withoutView>without</withoutView></root>";
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<HttpMessageConverter<?>> 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 = "<root>" +
"<withView1>with</withView1>" +
@ -661,6 +812,34 @@ class RequestResponseBodyMethodProcessorTests { @@ -661,6 +812,34 @@ class RequestResponseBodyMethodProcessorTests {
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodParameter = handlerMethod.getMethodParameters()[0];
List<HttpMessageConverter<?>> converters = List.of(new JacksonXmlHttpMessageConverter());
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(
converters, null, List.of(new JsonViewRequestBodyAdvice()));
@SuppressWarnings("unchecked")
HttpEntity<JacksonViewBean> result = (HttpEntity<JacksonViewBean>)
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 = "<root>" +
"<withView1>with</withView1>" +
"<withView2>with</withView2>" +
"<withoutView>without</withoutView></root>";
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<HttpMessageConverter<?>> converters = List.of(new MappingJackson2XmlHttpMessageConverter());
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(
converters, null, List.of(new JsonViewRequestBodyAdvice()));
@ -676,12 +855,29 @@ class RequestResponseBodyMethodProcessorTests { @@ -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<HttpMessageConverter<?>> 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<HttpMessageConverter<?>> converters = List.of(new MappingJackson2HttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
@ -693,13 +889,13 @@ class RequestResponseBodyMethodProcessorTests { @@ -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<HttpMessageConverter<?>> converters = List.of(new MappingJackson2HttpMessageConverter());
List<HttpMessageConverter<?>> converters = List.of(new JacksonJsonHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
Object returnValue = new JacksonController().handleSubType();
@ -711,11 +907,47 @@ class RequestResponseBodyMethodProcessorTests { @@ -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<HttpMessageConverter<?>> 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<HttpMessageConverter<?>> 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<HttpMessageConverter<?>> converters = List.of(new MappingJackson2HttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
@ -738,7 +970,7 @@ class RequestResponseBodyMethodProcessorTests { @@ -738,7 +970,7 @@ class RequestResponseBodyMethodProcessorTests {
HandlerMethod handlerMethod = new HandlerMethod(new MyControllerImplementingInterface(), method);
MethodParameter methodParameter = handlerMethod.getMethodParameters()[0];
List<HttpMessageConverter<?>> converters = List.of(new MappingJackson2HttpMessageConverter());
List<HttpMessageConverter<?>> converters = List.of(new JacksonJsonHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
assertThat(processor.supportsParameter(methodParameter)).isTrue();
@ -756,7 +988,7 @@ class RequestResponseBodyMethodProcessorTests { @@ -756,7 +988,7 @@ class RequestResponseBodyMethodProcessorTests {
HandlerMethod handlerMethod = new HandlerMethod(new SubControllerImplementingInterface(), method);
MethodParameter methodParameter = handlerMethod.getMethodParameters()[0];
List<HttpMessageConverter<?>> converters = List.of(new MappingJackson2HttpMessageConverter());
List<HttpMessageConverter<?>> converters = List.of(new JacksonJsonHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
assertThat(processor.supportsParameter(methodParameter)).isTrue();
@ -774,7 +1006,7 @@ class RequestResponseBodyMethodProcessorTests { @@ -774,7 +1006,7 @@ class RequestResponseBodyMethodProcessorTests {
HandlerMethod handlerMethod = new HandlerMethod(new SubControllerImplementingAbstractMethod(), method);
MethodParameter methodParameter = handlerMethod.getMethodParameters()[0];
List<HttpMessageConverter<?>> converters = List.of(new MappingJackson2HttpMessageConverter());
List<HttpMessageConverter<?>> converters = List.of(new JacksonJsonHttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
assertThat(processor.supportsParameter(methodParameter)).isTrue();
@ -1068,8 +1300,6 @@ class RequestResponseBodyMethodProcessorTests { @@ -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);
}

4
spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandlerTests.java

@ -35,7 +35,7 @@ import reactor.core.scheduler.Schedulers; @@ -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; @@ -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();

6
spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethodTests.java

@ -41,7 +41,7 @@ import org.springframework.http.ResponseEntity; @@ -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 { @@ -373,7 +373,7 @@ class ServletInvocableHandlerMethodTests {
@Test
void wrapConcurrentResult_CollectedValuesList() throws Exception {
List<HttpMessageConverter<?>> converters = Collections.singletonList(new MappingJackson2HttpMessageConverter());
List<HttpMessageConverter<?>> 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 { @@ -391,7 +391,7 @@ class ServletInvocableHandlerMethodTests {
@Test // SPR-15478
public void wrapConcurrentResult_CollectedValuesListWithResponseEntity() throws Exception {
List<HttpMessageConverter<?>> converters = Collections.singletonList(new MappingJackson2HttpMessageConverter());
List<HttpMessageConverter<?>> converters = Collections.singletonList(new JacksonJsonHttpMessageConverter());
ResolvableType elementType = ResolvableType.forClass(Bar.class);
ReactiveTypeHandler.CollectedValuesList result = new ReactiveTypeHandler.CollectedValuesList(elementType);
result.add(new Bar("foo"));

Loading…
Cancel
Save