Introduce Jackson 3 support for codecs

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

org.springframework.http.codec.json.Jackson2CodecSupport ->
org.springframework.http.codec.JacksonCodecSupport

org.springframework.http.codec.json.Jackson2Tokenizer ->
org.springframework.http.codec.JacksonTokenizer

org.springframework.http.codec.json.Jackson2SmileDecoder ->
org.springframework.http.codec.smile.JacksonSmileDecoder

org.springframework.http.codec.json.Jackson2SmileEncoder ->
org.springframework.http.codec.smile.JacksonSmileEncoder

Jackson2CborDecoder -> JacksonCborDecoder
Jackson2CborEncoder -> JacksonCborEncoder
Jackson2JsonDecoder -> JacksonJsonDecoder
Jackson2JsonEncoder -> JacksonJsonEncoder

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

See gh-33798
This commit is contained in:
Sébastien Deleuze
2025-05-13 12:27:02 +02:00
parent 746679f7a7
commit 5cb2f870d0
44 changed files with 3830 additions and 168 deletions

View File

@@ -0,0 +1,298 @@
/*
* 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.codec;
import java.lang.annotation.Annotation;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.context.ContextView;
import tools.jackson.core.JacksonException;
import tools.jackson.core.exc.JacksonIOException;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.JavaType;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectReader;
import tools.jackson.databind.cfg.MapperBuilder;
import tools.jackson.databind.exc.InvalidDefinitionException;
import tools.jackson.databind.util.TokenBuffer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.CodecException;
import org.springframework.core.codec.DecodingException;
import org.springframework.core.codec.Hints;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferLimitException;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.PooledDataBuffer;
import org.springframework.core.log.LogFormatUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
/**
* Abstract base class for Jackson 3.x decoding, leveraging non-blocking parsing.
*
* @author Sebastien Deleuze
* @since 7.0
*/
public abstract class AbstractJacksonDecoder extends JacksonCodecSupport implements HttpMessageDecoder<Object> {
private int maxInMemorySize = 256 * 1024;
/**
* 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 MimeType}s.
*/
protected AbstractJacksonDecoder(MapperBuilder<?, ?> builder, MimeType... mimeTypes) {
super(builder, mimeTypes);
}
/**
* Construct a new instance with the provided {@link ObjectMapper} and {@link MimeType}s.
*/
protected AbstractJacksonDecoder(ObjectMapper mapper, MimeType... mimeTypes) {
super(mapper, mimeTypes);
}
/**
* Set the max number of bytes that can be buffered by this decoder. This
* is either the size of the entire input when decoding as a whole, or the
* size of one top-level JSON object within a JSON stream. When the limit
* is exceeded, {@link DataBufferLimitException} is raised.
* <p>By default this is set to 256K.
* @param byteCount the max number of bytes to buffer, or -1 for unlimited
*/
public void setMaxInMemorySize(int byteCount) {
this.maxInMemorySize = byteCount;
}
/**
* Return the {@link #setMaxInMemorySize configured} byte count limit.
*/
public int getMaxInMemorySize() {
return this.maxInMemorySize;
}
@Override
public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) {
if (!supportsMimeType(mimeType)) {
return false;
}
ObjectMapper mapper = selectObjectMapper(elementType, mimeType);
if (mapper == null) {
return false;
}
return !CharSequence.class.isAssignableFrom(elementType.toClass());
}
@Override
public Flux<Object> decode(Publisher<DataBuffer> input, ResolvableType elementType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
ObjectMapper mapper = selectObjectMapper(elementType, mimeType);
if (mapper == null) {
return Flux.error(new IllegalStateException("No ObjectMapper for " + elementType));
}
boolean forceUseOfBigDecimal = mapper.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
if (BigDecimal.class.equals(elementType.getType())) {
forceUseOfBigDecimal = true;
}
boolean tokenizeArrays = (!elementType.isArray() &&
!Collection.class.isAssignableFrom(elementType.resolve(Object.class)));
Flux<DataBuffer> processed = processInput(input, elementType, mimeType, hints);
Flux<TokenBuffer> tokens = JacksonTokenizer.tokenize(processed, mapper,
tokenizeArrays, forceUseOfBigDecimal, getMaxInMemorySize());
return Flux.deferContextual(contextView -> {
Map<String, Object> hintsToUse = contextView.isEmpty() ? hints :
Hints.merge(hints, ContextView.class.getName(), contextView);
ObjectReader reader = createObjectReader(mapper, elementType, hintsToUse);
return tokens.handle((tokenBuffer, sink) -> {
try {
Object value = reader.readValue(tokenBuffer.asParser(getObjectMapper()._deserializationContext()));
logValue(value, hints);
if (value != null) {
sink.next(value);
}
}
catch (JacksonException ex) {
sink.error(processException(ex));
}
})
.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release);
});
}
/**
* Process the input publisher into a flux. Default implementation returns
* {@link Flux#from(Publisher)}, but subclasses can choose to customize
* this behavior.
* @param input the {@code DataBuffer} input stream to process
* @param elementType the expected type of elements in the output stream
* @param mimeType the MIME type associated with the input stream (optional)
* @param hints additional information about how to do encode
* @return the processed flux
*/
protected Flux<DataBuffer> processInput(Publisher<DataBuffer> input, ResolvableType elementType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
return Flux.from(input);
}
@Override
public Mono<Object> decodeToMono(Publisher<DataBuffer> input, ResolvableType elementType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
return Mono.deferContextual(contextView -> {
Map<String, Object> hintsToUse = contextView.isEmpty() ? hints :
Hints.merge(hints, ContextView.class.getName(), contextView);
return DataBufferUtils.join(input, this.maxInMemorySize).flatMap(dataBuffer ->
Mono.justOrEmpty(decode(dataBuffer, elementType, mimeType, hintsToUse)));
});
}
@Override
public Object decode(DataBuffer dataBuffer, ResolvableType targetType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) throws DecodingException {
ObjectMapper mapper = selectObjectMapper(targetType, mimeType);
if (mapper == null) {
throw new IllegalStateException("No ObjectMapper for " + targetType);
}
try {
ObjectReader objectReader = createObjectReader(mapper, targetType, hints);
Object value = objectReader.readValue(dataBuffer.asInputStream());
logValue(value, hints);
return value;
}
catch (JacksonException ex) {
throw processException(ex);
}
finally {
DataBufferUtils.release(dataBuffer);
}
}
private ObjectReader createObjectReader(
ObjectMapper mapper, ResolvableType elementType, @Nullable Map<String, Object> hints) {
Assert.notNull(elementType, "'elementType' must not be null");
Class<?> contextClass = getContextClass(elementType);
if (contextClass == null && hints != null) {
contextClass = getContextClass((ResolvableType) hints.get(ACTUAL_TYPE_HINT));
}
JavaType javaType = getJavaType(elementType.getType(), contextClass);
Class<?> jsonView = (hints != null ? (Class<?>) hints.get(JacksonCodecSupport.JSON_VIEW_HINT) : null);
ObjectReader objectReader = (jsonView != null ?
mapper.readerWithView(jsonView).forType(javaType) :
mapper.readerFor(javaType));
return customizeReader(objectReader, elementType, hints);
}
/**
* Subclasses can use this method to customize {@link ObjectReader} used
* for reading values.
* @param reader the reader instance to customize
* @param elementType the target type of element values to read to
* @param hints a map with serialization hints;
* the Reactor Context, when available, may be accessed under the key
* {@code ContextView.class.getName()}
* @return the customized {@code ObjectReader} to use
*/
protected ObjectReader customizeReader(
ObjectReader reader, ResolvableType elementType, @Nullable Map<String, Object> hints) {
return reader;
}
private @Nullable Class<?> getContextClass(@Nullable ResolvableType elementType) {
MethodParameter param = (elementType != null ? getParameter(elementType) : null);
return (param != null ? param.getContainingClass() : null);
}
private void logValue(@Nullable Object value, @Nullable Map<String, Object> hints) {
if (!Hints.isLoggingSuppressed(hints)) {
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(value, !traceOn);
return Hints.getLogPrefix(hints) + "Decoded [" + formatted + "]";
});
}
}
private CodecException processException(JacksonException ex) {
if (ex instanceof InvalidDefinitionException ide) {
JavaType type = ide.getType();
return new CodecException("Type definition error: " + type, ex);
}
if (ex instanceof JacksonIOException) {
return new DecodingException("I/O error while parsing input stream", ex);
}
String originalMessage = ex.getOriginalMessage();
return new DecodingException("JSON decoding error: " + originalMessage, ex);
}
// HttpMessageDecoder
@Override
public Map<String, Object> getDecodeHints(ResolvableType actualType, ResolvableType elementType,
ServerHttpRequest request, ServerHttpResponse response) {
return getHints(actualType);
}
@Override
public List<MimeType> getDecodableMimeTypes() {
return getMimeTypes();
}
@Override
public List<MimeType> getDecodableMimeTypes(ResolvableType targetType) {
return getMimeTypes(targetType);
}
// JacksonCodecSupport
@Override
protected <A extends Annotation> @Nullable A getAnnotation(MethodParameter parameter, Class<A> annotType) {
return parameter.getParameterAnnotation(annotType);
}
}

View File

@@ -0,0 +1,444 @@
/*
* 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.codec;
import java.lang.annotation.Annotation;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.context.ContextView;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonEncoding;
import tools.jackson.core.JsonGenerator;
import tools.jackson.core.exc.JacksonIOException;
import tools.jackson.core.util.ByteArrayBuilder;
import tools.jackson.databind.JavaType;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectWriter;
import tools.jackson.databind.SequenceWriter;
import tools.jackson.databind.cfg.MapperBuilder;
import tools.jackson.databind.exc.InvalidDefinitionException;
import tools.jackson.databind.ser.FilterProvider;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.CodecException;
import org.springframework.core.codec.EncodingException;
import org.springframework.core.codec.Hints;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.log.LogFormatUtils;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MimeType;
/**
* Base class providing support methods for Jackson 3.x encoding. For non-streaming use
* cases, {@link Flux} elements are collected into a {@link List} before serialization for
* performance reasons.
*
* @author Sebastien Deleuze
* @since 7.0
*/
public abstract class AbstractJacksonEncoder extends JacksonCodecSupport implements HttpMessageEncoder<Object> {
private static final byte[] NEWLINE_SEPARATOR = {'\n'};
private static final byte[] EMPTY_BYTES = new byte[0];
private static final Map<String, JsonEncoding> ENCODINGS;
static {
ENCODINGS = CollectionUtils.newHashMap(JsonEncoding.values().length);
for (JsonEncoding encoding : JsonEncoding.values()) {
ENCODINGS.put(encoding.getJavaName(), encoding);
}
ENCODINGS.put("US-ASCII", JsonEncoding.UTF8);
}
private final List<MediaType> streamingMediaTypes = new ArrayList<>(1);
/**
* 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 MimeType}s.
*/
protected AbstractJacksonEncoder(MapperBuilder<?, ?> builder, MimeType... mimeTypes) {
super(builder, mimeTypes);
}
/**
* Construct a new instance with the provided {@link ObjectMapper} and {@link MimeType}s.
*/
protected AbstractJacksonEncoder(ObjectMapper mapper, MimeType... mimeTypes) {
super(mapper, mimeTypes);
}
/**
* Configure "streaming" media types for which flushing should be performed
* automatically vs at the end of the stream.
*/
public void setStreamingMediaTypes(List<MediaType> mediaTypes) {
this.streamingMediaTypes.clear();
this.streamingMediaTypes.addAll(mediaTypes);
}
@Override
public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) {
if (!supportsMimeType(mimeType)) {
return false;
}
if (mimeType != null && mimeType.getCharset() != null) {
Charset charset = mimeType.getCharset();
if (!ENCODINGS.containsKey(charset.name())) {
return false;
}
}
if (this.objectMapperRegistrations != null && selectObjectMapper(elementType, mimeType) == null) {
return false;
}
Class<?> clazz = elementType.resolve();
if (clazz == null) {
return true;
}
if (MappingJacksonValue.class.isAssignableFrom(elementType.resolve(clazz))) {
throw new UnsupportedOperationException("MappingJacksonValue is not supported, use hints instead");
}
return !String.class.isAssignableFrom(elementType.resolve(clazz));
}
@Override
public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory,
ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
Assert.notNull(inputStream, "'inputStream' must not be null");
Assert.notNull(bufferFactory, "'bufferFactory' must not be null");
Assert.notNull(elementType, "'elementType' must not be null");
return Flux.deferContextual(contextView -> {
Map<String, Object> hintsToUse = contextView.isEmpty() ? hints :
Hints.merge(hints, ContextView.class.getName(), contextView);
if (inputStream instanceof Mono) {
return Mono.from(inputStream)
.map(value -> encodeValue(value, bufferFactory, elementType, mimeType, hintsToUse))
.flux();
}
try {
ObjectMapper mapper = selectObjectMapper(elementType, mimeType);
if (mapper == null) {
throw new IllegalStateException("No ObjectMapper for " + elementType);
}
ObjectWriter writer = createObjectWriter(mapper, elementType, mimeType, null, hintsToUse);
ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer.generatorFactory()._getBufferRecycler());
JsonEncoding encoding = getJsonEncoding(mimeType);
JsonGenerator generator = mapper.createGenerator(byteBuilder, encoding);
SequenceWriter sequenceWriter = writer.writeValues(generator);
byte[] separator = getStreamingMediaTypeSeparator(mimeType);
Flux<DataBuffer> dataBufferFlux;
if (separator != null) {
dataBufferFlux = Flux.from(inputStream).map(value -> encodeStreamingValue(
value, bufferFactory, hintsToUse, sequenceWriter, byteBuilder, EMPTY_BYTES, separator));
}
else {
JsonArrayJoinHelper helper = new JsonArrayJoinHelper();
// Do not prepend JSON array prefix until first signal is known, onNext vs onError
// Keeps response not committed for error handling
dataBufferFlux = Flux.from(inputStream)
.map(value -> {
byte[] prefix = helper.getPrefix();
byte[] delimiter = helper.getDelimiter();
DataBuffer dataBuffer = encodeStreamingValue(
value, bufferFactory, hintsToUse, sequenceWriter, byteBuilder,
delimiter, EMPTY_BYTES);
return (prefix.length > 0 ?
bufferFactory.join(List.of(bufferFactory.wrap(prefix), dataBuffer)) :
dataBuffer);
})
.switchIfEmpty(Mono.fromCallable(() -> bufferFactory.wrap(helper.getPrefix())))
.concatWith(Mono.fromCallable(() -> bufferFactory.wrap(helper.getSuffix())));
}
return dataBufferFlux
.doOnNext(dataBuffer -> Hints.touchDataBuffer(dataBuffer, hintsToUse, logger))
.doAfterTerminate(() -> {
try {
generator.close();
byteBuilder.release();
}
catch (JacksonIOException ex) {
logger.error("Could not close Encoder resources", ex);
}
});
}
catch (JacksonIOException ex) {
return Flux.error(ex);
}
});
}
@Override
public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory,
ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
Class<?> jsonView = null;
FilterProvider filters = null;
if (hints != null) {
jsonView = (Class<?>) hints.get(JSON_VIEW_HINT);
filters = (FilterProvider) hints.get(FILTER_PROVIDER_HINT);
}
ObjectMapper mapper = selectObjectMapper(valueType, mimeType);
if (mapper == null) {
throw new IllegalStateException("No ObjectMapper for " + valueType);
}
ObjectWriter writer = createObjectWriter(mapper, valueType, mimeType, jsonView, hints);
if (filters != null) {
writer = writer.with(filters);
}
ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer.generatorFactory()._getBufferRecycler());
try {
JsonEncoding encoding = getJsonEncoding(mimeType);
logValue(hints, value);
try (JsonGenerator generator = writer.createGenerator(byteBuilder, encoding)) {
writer.writeValue(generator, value);
generator.flush();
}
catch (InvalidDefinitionException ex) {
throw new CodecException("Type definition error: " + ex.getType(), ex);
}
catch (JacksonException ex) {
throw new EncodingException("JSON encoding error: " + ex.getOriginalMessage(), ex);
}
byte[] bytes = byteBuilder.toByteArray();
DataBuffer buffer = bufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
Hints.touchDataBuffer(buffer, hints, logger);
return buffer;
}
finally {
byteBuilder.release();
}
}
private DataBuffer encodeStreamingValue(
Object value, DataBufferFactory bufferFactory, @Nullable Map<String, Object> hints,
SequenceWriter sequenceWriter, ByteArrayBuilder byteArrayBuilder,
byte[] prefix, byte[] suffix) {
logValue(hints, value);
try {
sequenceWriter.write(value);
sequenceWriter.flush();
}
catch (InvalidDefinitionException ex) {
throw new CodecException("Type definition error: " + ex.getType(), ex);
}
catch (JacksonException ex) {
throw new EncodingException("JSON encoding error: " + ex.getOriginalMessage(), ex);
}
byte[] bytes = byteArrayBuilder.toByteArray();
byteArrayBuilder.reset();
int offset;
int length;
if (bytes.length > 0 && bytes[0] == ' ') {
// SequenceWriter writes an unnecessary space in between values
offset = 1;
length = bytes.length - 1;
}
else {
offset = 0;
length = bytes.length;
}
DataBuffer buffer = bufferFactory.allocateBuffer(length + prefix.length + suffix.length);
if (prefix.length != 0) {
buffer.write(prefix);
}
buffer.write(bytes, offset, length);
if (suffix.length != 0) {
buffer.write(suffix);
}
Hints.touchDataBuffer(buffer, hints, logger);
return buffer;
}
private void logValue(@Nullable Map<String, Object> hints, Object value) {
if (!Hints.isLoggingSuppressed(hints)) {
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(value, !traceOn);
return Hints.getLogPrefix(hints) + "Encoding [" + formatted + "]";
});
}
}
private ObjectWriter createObjectWriter(
ObjectMapper mapper, ResolvableType valueType, @Nullable MimeType mimeType,
@Nullable Class<?> jsonView, @Nullable Map<String, Object> hints) {
JavaType javaType = getJavaType(valueType.getType(), null);
if (jsonView == null && hints != null) {
jsonView = (Class<?>) hints.get(JacksonCodecSupport.JSON_VIEW_HINT);
}
ObjectWriter writer = (jsonView != null ? mapper.writerWithView(jsonView) : mapper.writer());
if (javaType.isContainerType()) {
writer = writer.forType(javaType);
}
return customizeWriter(writer, mimeType, valueType, hints);
}
/**
* Subclasses can use this method to customize the {@link ObjectWriter} used
* for writing values.
* @param writer the writer instance to customize
* @param mimeType the selected MIME type
* @param elementType the type of element values to write
* @param hints a map with serialization hints; the Reactor Context, when
* available, may be accessed under the key
* {@code ContextView.class.getName()}
* @return the customized {@code ObjectWriter} to use
*/
protected ObjectWriter customizeWriter(ObjectWriter writer, @Nullable MimeType mimeType,
ResolvableType elementType, @Nullable Map<String, Object> hints) {
return writer;
}
/**
* Return the separator to use for the given mime type.
* <p>By default, this method returns new line {@code "\n"} if the given
* mime type is one of the configured {@link #setStreamingMediaTypes(List)
* streaming} mime types.
*/
protected byte @Nullable [] getStreamingMediaTypeSeparator(@Nullable MimeType mimeType) {
for (MediaType streamingMediaType : this.streamingMediaTypes) {
if (streamingMediaType.isCompatibleWith(mimeType)) {
return NEWLINE_SEPARATOR;
}
}
return null;
}
/**
* Determine the JSON encoding to use for the given mime type.
* @param mimeType the mime type as requested by the caller
* @return the JSON encoding to use (never {@code null})
*/
protected JsonEncoding getJsonEncoding(@Nullable MimeType mimeType) {
if (mimeType != null && mimeType.getCharset() != null) {
Charset charset = mimeType.getCharset();
JsonEncoding result = ENCODINGS.get(charset.name());
if (result != null) {
return result;
}
}
return JsonEncoding.UTF8;
}
// HttpMessageEncoder
@Override
public List<MimeType> getEncodableMimeTypes() {
return getMimeTypes();
}
@Override
public List<MimeType> getEncodableMimeTypes(ResolvableType elementType) {
return getMimeTypes(elementType);
}
@Override
public List<MediaType> getStreamingMediaTypes() {
return Collections.unmodifiableList(this.streamingMediaTypes);
}
@Override
public Map<String, Object> getEncodeHints(@Nullable ResolvableType actualType, ResolvableType elementType,
@Nullable MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) {
return (actualType != null ? getHints(actualType) : Hints.none());
}
// JacksonCodecSupport
@Override
protected <A extends Annotation> @Nullable A getAnnotation(MethodParameter parameter, Class<A> annotType) {
return parameter.getMethodAnnotation(annotType);
}
private static class JsonArrayJoinHelper {
private static final byte[] COMMA_SEPARATOR = {','};
private static final byte[] OPEN_BRACKET = {'['};
private static final byte[] CLOSE_BRACKET = {']'};
private boolean firstItemEmitted;
public byte[] getDelimiter() {
if (this.firstItemEmitted) {
return COMMA_SEPARATOR;
}
this.firstItemEmitted = true;
return EMPTY_BYTES;
}
public byte[] getPrefix() {
return (this.firstItemEmitted ? EMPTY_BYTES : OPEN_BRACKET);
}
public byte[] getSuffix() {
return CLOSE_BRACKET;
}
}
}

View File

@@ -23,6 +23,8 @@ import org.jspecify.annotations.Nullable;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.http.codec.smile.JacksonSmileDecoder;
import org.springframework.http.codec.smile.JacksonSmileEncoder;
/**
* Defines a common interface for configuring either client or server HTTP
@@ -109,7 +111,16 @@ public interface CodecConfigurer {
interface DefaultCodecs {
/**
* Override the default Jackson JSON {@code Decoder}.
* Override the default Jackson 3.x JSON {@code Decoder}.
* <p>Note that {@link #maxInMemorySize(int)}, if configured, will be
* applied to the given decoder.
* @param decoder the decoder instance to use
* @see org.springframework.http.codec.json.JacksonJsonDecoder
*/
void jacksonJsonDecoder(Decoder<?> decoder);
/**
* Override the default Jackson 2.x JSON {@code Decoder}.
* <p>Note that {@link #maxInMemorySize(int)}, if configured, will be
* applied to the given decoder.
* @param decoder the decoder instance to use
@@ -118,14 +129,30 @@ public interface CodecConfigurer {
void jackson2JsonDecoder(Decoder<?> decoder);
/**
* Override the default Jackson JSON {@code Encoder}.
* Override the default Jackson 3.x JSON {@code Encoder}.
* @param encoder the encoder instance to use
* @see org.springframework.http.codec.json.JacksonJsonEncoder
*/
void jacksonJsonEncoder(Encoder<?> encoder);
/**
* Override the default Jackson 2.x JSON {@code Encoder}.
* @param encoder the encoder instance to use
* @see org.springframework.http.codec.json.Jackson2JsonEncoder
*/
void jackson2JsonEncoder(Encoder<?> encoder);
/**
* Override the default Jackson Smile {@code Decoder}.
* Override the default Jackson 3.x Smile {@code Decoder}.
* <p>Note that {@link #maxInMemorySize(int)}, if configured, will be
* applied to the given decoder.
* @param decoder the decoder instance to use
* @see JacksonSmileDecoder
*/
void jacksonSmileDecoder(Decoder<?> decoder);
/**
* Override the default Jackson 2.x Smile {@code Decoder}.
* <p>Note that {@link #maxInMemorySize(int)}, if configured, will be
* applied to the given decoder.
* @param decoder the decoder instance to use
@@ -134,7 +161,14 @@ public interface CodecConfigurer {
void jackson2SmileDecoder(Decoder<?> decoder);
/**
* Override the default Jackson Smile {@code Encoder}.
* Override the default Jackson 3.x Smile {@code Encoder}.
* @param encoder the encoder instance to use
* @see JacksonSmileEncoder
*/
void jacksonSmileEncoder(Encoder<?> encoder);
/**
* Override the default Jackson 2.x Smile {@code Encoder}.
* @param encoder the encoder instance to use
* @see org.springframework.http.codec.json.Jackson2SmileEncoder
*/

View File

@@ -0,0 +1,275 @@
/*
* 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.codec;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import com.fasterxml.jackson.annotation.JsonView;
import org.apache.commons.logging.Log;
import org.jspecify.annotations.Nullable;
import tools.jackson.databind.JacksonModule;
import tools.jackson.databind.JavaType;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.cfg.MapperBuilder;
import tools.jackson.databind.ser.FilterProvider;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Hints;
import org.springframework.http.HttpLogging;
import org.springframework.http.MediaType;
import org.springframework.http.ProblemDetail;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MimeType;
/**
* Base class providing support methods for Jackson 2.x encoding and decoding.
*
* @author Sebastien Deleuze
* @since 7.0
*/
public abstract class JacksonCodecSupport {
/**
* The key for the hint to specify a "JSON View" for encoding or decoding
* with the value expected to be a {@link Class}.
*/
public static final String JSON_VIEW_HINT = JsonView.class.getName();
/**
* The key for the hint to specify a {@link FilterProvider}.
*/
public static final String FILTER_PROVIDER_HINT = FilterProvider.class.getName();
/**
* The key for the hint to access the actual ResolvableType passed into
* {@link org.springframework.http.codec.HttpMessageReader#read(ResolvableType, ResolvableType, ServerHttpRequest, ServerHttpResponse, Map)}
* (server-side only). Currently set when the method argument has generics because
* in case of reactive types, use of {@code ResolvableType.getGeneric()} means no
* MethodParameter source and no knowledge of the containing class.
*/
static final String ACTUAL_TYPE_HINT = JacksonCodecSupport.class.getName() + ".actualType";
private static final String JSON_VIEW_HINT_ERROR =
"@JsonView only supported for write hints with exactly 1 class argument: ";
protected final Log logger = HttpLogging.forLogName(getClass());
private final ObjectMapper defaultObjectMapper;
protected @Nullable Map<Class<?>, Map<MimeType, ObjectMapper>> objectMapperRegistrations;
private final List<MimeType> mimeTypes;
private static volatile @Nullable List<JacksonModule> modules = null;
/**
* 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 MimeType}s.
*/
protected JacksonCodecSupport(MapperBuilder<?, ?> builder, MimeType... mimeTypes) {
Assert.notNull(builder, "MapperBuilder must not be null");
Assert.notEmpty(mimeTypes, "MimeTypes must not be empty");
this.defaultObjectMapper = builder.addModules(initModules()).build();
this.mimeTypes = List.of(mimeTypes);
}
/**
* Construct a new instance with the provided {@link ObjectMapper}
* customized with the {@link tools.jackson.databind.JacksonModule}s found
* by {@link MapperBuilder#findModules(ClassLoader)} and {@link MimeType}s.
*/
protected JacksonCodecSupport(ObjectMapper objectMapper, MimeType... mimeTypes) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
Assert.notEmpty(mimeTypes, "MimeTypes must not be empty");
this.defaultObjectMapper = objectMapper;
this.mimeTypes = List.of(mimeTypes);
}
private List<JacksonModule> initModules() {
if (modules == null) {
modules = MapperBuilder.findModules(JacksonCodecSupport.class.getClassLoader());
}
return Objects.requireNonNull(modules);
}
/**
* Return the {@link ObjectMapper configured} default ObjectMapper.
*/
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 supported
* {@link #getMimeTypes() MimeTypes} 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<MimeType, ObjectMapper>> registrar) {
if (this.objectMapperRegistrations == null) {
this.objectMapperRegistrations = new LinkedHashMap<>();
}
Map<MimeType, 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 @Nullable Map<MimeType, ObjectMapper> getObjectMappersForType(Class<?> clazz) {
for (Map.Entry<Class<?>, Map<MimeType, ObjectMapper>> entry : getObjectMapperRegistrations().entrySet()) {
if (entry.getKey().isAssignableFrom(clazz)) {
return entry.getValue();
}
}
return Collections.emptyMap();
}
protected Map<Class<?>, Map<MimeType, ObjectMapper>> getObjectMapperRegistrations() {
return (this.objectMapperRegistrations != null ? this.objectMapperRegistrations : Collections.emptyMap());
}
/**
* Subclasses should expose this as "decodable" or "encodable" mime types.
*/
protected List<MimeType> getMimeTypes() {
return this.mimeTypes;
}
protected List<MimeType> getMimeTypes(ResolvableType elementType) {
Class<?> elementClass = elementType.toClass();
List<MimeType> result = null;
for (Map.Entry<Class<?>, Map<MimeType, ObjectMapper>> entry : getObjectMapperRegistrations().entrySet()) {
if (entry.getKey().isAssignableFrom(elementClass)) {
result = (result != null ? result : new ArrayList<>(entry.getValue().size()));
result.addAll(entry.getValue().keySet());
}
}
if (!CollectionUtils.isEmpty(result)) {
return result;
}
return (ProblemDetail.class.isAssignableFrom(elementClass) ? getMediaTypesForProblemDetail() : getMimeTypes());
}
/**
* Return the supported media type(s) for {@link ProblemDetail}.
* By default, an empty list, unless overridden in subclasses.
*/
protected List<MimeType> getMediaTypesForProblemDetail() {
return Collections.emptyList();
}
protected boolean supportsMimeType(@Nullable MimeType mimeType) {
if (mimeType == null) {
return true;
}
for (MimeType supportedMimeType : this.mimeTypes) {
if (supportedMimeType.isCompatibleWith(mimeType)) {
return true;
}
}
return false;
}
protected JavaType getJavaType(Type type, @Nullable Class<?> contextClass) {
return this.defaultObjectMapper.constructType(GenericTypeResolver.resolveType(type, contextClass));
}
protected Map<String, Object> getHints(ResolvableType resolvableType) {
MethodParameter param = getParameter(resolvableType);
if (param != null) {
Map<String, Object> hints = null;
if (resolvableType.hasGenerics()) {
hints = new HashMap<>(2);
hints.put(ACTUAL_TYPE_HINT, resolvableType);
}
JsonView annotation = getAnnotation(param, JsonView.class);
if (annotation != null) {
Class<?>[] classes = annotation.value();
Assert.isTrue(classes.length == 1, () -> JSON_VIEW_HINT_ERROR + param);
hints = (hints != null ? hints : new HashMap<>(1));
hints.put(JSON_VIEW_HINT, classes[0]);
}
if (hints != null) {
return hints;
}
}
return Hints.none();
}
protected @Nullable MethodParameter getParameter(ResolvableType type) {
return (type.getSource() instanceof MethodParameter methodParameter ? methodParameter : null);
}
protected abstract <A extends Annotation> @Nullable A getAnnotation(MethodParameter parameter, Class<A> annotType);
/**
* 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)}.
*/
protected @Nullable ObjectMapper selectObjectMapper(ResolvableType targetType, @Nullable MimeType targetMimeType) {
if (targetMimeType == null || CollectionUtils.isEmpty(this.objectMapperRegistrations)) {
return this.defaultObjectMapper;
}
Class<?> targetClass = targetType.toClass();
for (Map.Entry<Class<?>, Map<MimeType, ObjectMapper>> typeEntry : getObjectMapperRegistrations().entrySet()) {
if (typeEntry.getKey().isAssignableFrom(targetClass)) {
for (Map.Entry<MimeType, ObjectMapper> objectMapperEntry : typeEntry.getValue().entrySet()) {
if (objectMapperEntry.getKey().includes(targetMimeType)) {
return objectMapperEntry.getValue();
}
}
// No matching registrations
return null;
}
}
// No registrations
return this.defaultObjectMapper;
}
}

View File

@@ -0,0 +1,241 @@
/*
* 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.codec;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import reactor.core.publisher.Flux;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.core.async.ByteArrayFeeder;
import tools.jackson.core.async.ByteBufferFeeder;
import tools.jackson.core.async.NonBlockingInputFeeder;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.util.TokenBuffer;
import org.springframework.core.codec.DecodingException;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferLimitException;
import org.springframework.core.io.buffer.DataBufferUtils;
/**
* {@link Function} to transform a JSON stream of arbitrary size, byte array
* chunks into a {@code Flux<TokenBuffer>} where each token buffer is a
* well-formed JSON object with Jackson 3.x.
*
* @author Sebastien Deleuze
* @since 7.0
*/
final class JacksonTokenizer {
private final JsonParser parser;
private final NonBlockingInputFeeder inputFeeder;
private final boolean tokenizeArrayElements;
private final boolean forceUseOfBigDecimal;
private final int maxInMemorySize;
private int objectDepth;
private int arrayDepth;
private int byteCount;
private TokenBuffer tokenBuffer;
private JacksonTokenizer(JsonParser parser, boolean tokenizeArrayElements, boolean forceUseOfBigDecimal, int maxInMemorySize) {
this.parser = parser;
this.inputFeeder = this.parser.nonBlockingInputFeeder();
this.tokenizeArrayElements = tokenizeArrayElements;
this.forceUseOfBigDecimal = forceUseOfBigDecimal;
this.maxInMemorySize = maxInMemorySize;
this.tokenBuffer = createToken();
}
private List<TokenBuffer> tokenize(DataBuffer dataBuffer) {
try {
int bufferSize = dataBuffer.readableByteCount();
List<TokenBuffer> tokens = new ArrayList<>();
if (this.inputFeeder instanceof ByteBufferFeeder byteBufferFeeder) {
try (DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers()) {
while (iterator.hasNext()) {
byteBufferFeeder.feedInput(iterator.next());
parseTokens(tokens);
}
}
}
else if (this.inputFeeder instanceof ByteArrayFeeder byteArrayFeeder) {
byte[] bytes = new byte[bufferSize];
dataBuffer.read(bytes);
byteArrayFeeder.feedInput(bytes, 0, bufferSize);
parseTokens(tokens);
}
assertInMemorySize(bufferSize, tokens);
return tokens;
}
catch (JacksonException ex) {
throw new DecodingException("JSON decoding error: " + ex.getOriginalMessage(), ex);
}
finally {
DataBufferUtils.release(dataBuffer);
}
}
private Flux<TokenBuffer> endOfInput() {
return Flux.defer(() -> {
this.inputFeeder.endOfInput();
try {
List<TokenBuffer> tokens = new ArrayList<>();
parseTokens(tokens);
return Flux.fromIterable(tokens);
}
catch (JacksonException ex) {
throw new DecodingException("JSON decoding error: " + ex.getOriginalMessage(), ex);
}
});
}
private void parseTokens(List<TokenBuffer> tokens) {
// SPR-16151: Smile data format uses null to separate documents
boolean previousNull = false;
while (!this.parser.isClosed()) {
JsonToken token = this.parser.nextToken();
if (token == JsonToken.NOT_AVAILABLE ||
token == null && previousNull) {
break;
}
else if (token == null ) { // !previousNull
previousNull = true;
continue;
}
else {
previousNull = false;
}
updateDepth(token);
if (!this.tokenizeArrayElements) {
processTokenNormal(token, tokens);
}
else {
processTokenArray(token, tokens);
}
}
}
private void updateDepth(JsonToken token) {
switch (token) {
case START_OBJECT -> this.objectDepth++;
case END_OBJECT -> this.objectDepth--;
case START_ARRAY -> this.arrayDepth++;
case END_ARRAY -> this.arrayDepth--;
}
}
private void processTokenNormal(JsonToken token, List<TokenBuffer> result) {
this.tokenBuffer.copyCurrentEvent(this.parser);
if ((token.isStructEnd() || token.isScalarValue()) && this.objectDepth == 0 && this.arrayDepth == 0) {
result.add(this.tokenBuffer);
this.tokenBuffer = createToken();
}
}
private void processTokenArray(JsonToken token, List<TokenBuffer> result) {
if (!isTopLevelArrayToken(token)) {
this.tokenBuffer.copyCurrentEvent(this.parser);
}
if (this.objectDepth == 0 && (this.arrayDepth == 0 || this.arrayDepth == 1) &&
(token == JsonToken.END_OBJECT || token.isScalarValue())) {
result.add(this.tokenBuffer);
this.tokenBuffer = createToken();
}
}
private TokenBuffer createToken() {
TokenBuffer tokenBuffer = TokenBuffer.forBuffering(this.parser, this.parser.objectReadContext());
tokenBuffer.forceUseOfBigDecimal(this.forceUseOfBigDecimal);
return tokenBuffer;
}
private boolean isTopLevelArrayToken(JsonToken token) {
return this.objectDepth == 0 && ((token == JsonToken.START_ARRAY && this.arrayDepth == 1) ||
(token == JsonToken.END_ARRAY && this.arrayDepth == 0));
}
private void assertInMemorySize(int currentBufferSize, List<TokenBuffer> result) {
if (this.maxInMemorySize >= 0) {
if (!result.isEmpty()) {
this.byteCount = 0;
}
else if (currentBufferSize > Integer.MAX_VALUE - this.byteCount) {
raiseLimitException();
}
else {
this.byteCount += currentBufferSize;
if (this.byteCount > this.maxInMemorySize) {
raiseLimitException();
}
}
}
}
private void raiseLimitException() {
throw new DataBufferLimitException(
"Exceeded limit on max bytes per JSON object: " + this.maxInMemorySize);
}
/**
* Tokenize the given {@code Flux<DataBuffer>} into {@code Flux<TokenBuffer>}.
* @param dataBuffers the source data buffers
* @param objectMapper the current mapper instance
* @param tokenizeArrays if {@code true} and the "top level" JSON object is
* an array, each element is returned individually immediately after it is received
* @param forceUseOfBigDecimal if {@code true}, any floating point values encountered
* in source will use {@link java.math.BigDecimal}
* @param maxInMemorySize maximum memory size
* @return the resulting token buffers
*/
public static Flux<TokenBuffer> tokenize(Flux<DataBuffer> dataBuffers,
ObjectMapper objectMapper, boolean tokenizeArrays, boolean forceUseOfBigDecimal, int maxInMemorySize) {
try {
JsonParser parser;
try {
parser = objectMapper.createNonBlockingByteBufferParser();
}
catch (UnsupportedOperationException ex) {
parser = objectMapper.createNonBlockingByteArrayParser();
}
JacksonTokenizer tokenizer =
new JacksonTokenizer(parser, tokenizeArrays, forceUseOfBigDecimal, maxInMemorySize);
return dataBuffers.concatMapIterable(tokenizer::tokenize).concatWith(tokenizer.endOfInput());
}
catch (JacksonException ex) {
return Flux.error(ex);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -33,7 +33,7 @@ import org.springframework.util.Assert;
import org.springframework.util.MimeType;
/**
* Decode bytes into CBOR and convert to Object's with Jackson.
* Decode bytes into CBOR and convert to Object's with Jackson 2.x.
* Stream decoding is not supported yet.
*
* @author Sebastien Deleuze

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -34,7 +34,7 @@ import org.springframework.util.Assert;
import org.springframework.util.MimeType;
/**
* Encode from an {@code Object} to bytes of CBOR objects using Jackson.
* Encode from an {@code Object} to bytes of CBOR objects using Jackson 2.x.
* Stream encoding is not supported yet.
*
* @author Sebastien Deleuze

View File

@@ -0,0 +1,76 @@
/*
* 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.codec.cbor;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import tools.jackson.databind.cfg.MapperBuilder;
import tools.jackson.dataformat.cbor.CBORMapper;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.MediaType;
import org.springframework.http.codec.AbstractJacksonDecoder;
import org.springframework.util.MimeType;
/**
* Decode bytes into CBOR and convert to Object's with Jackson 3.x.
* Stream decoding is not supported yet.
*
* @author Sebastien Deleuze
* @since 7.0
* @see JacksonCborEncoder
* @see <a href="https://github.com/spring-projects/spring-framework/issues/20513">Add CBOR support to WebFlux</a>
*/
public class JacksonCborDecoder extends AbstractJacksonDecoder {
/**
* Construct a new instance with a {@link CBORMapper} customized with the
* {@link tools.jackson.databind.JacksonModule}s found by
* {@link MapperBuilder#findModules(ClassLoader)}.
*/
public JacksonCborDecoder() {
super(CBORMapper.builder(), MediaType.APPLICATION_CBOR);
}
/**
* Construct a new instance with the provided {@link CBORMapper}.
*/
public JacksonCborDecoder(CBORMapper mapper) {
super(mapper, MediaType.APPLICATION_CBOR);
}
/**
* Construct a new instance with the provided {@link CBORMapper} and {@link MimeType}s.
* @see CBORMapper#builder()
* @see MapperBuilder#findAndAddModules(ClassLoader)
*/
public JacksonCborDecoder(CBORMapper mapper, MimeType... mimeTypes) {
super(mapper, mimeTypes);
}
@Override
public Flux<Object> decode(Publisher<DataBuffer> input, ResolvableType elementType, @Nullable MimeType mimeType,
@Nullable Map<String, Object> hints) {
throw new UnsupportedOperationException("Does not support stream decoding yet");
}
}

View File

@@ -0,0 +1,79 @@
/*
* 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.codec.cbor;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import tools.jackson.databind.cfg.MapperBuilder;
import tools.jackson.dataformat.cbor.CBORMapper;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.codec.AbstractJacksonEncoder;
import org.springframework.util.MimeType;
/**
* Encode from an {@code Object} to bytes of CBOR objects using Jackson 3.x.
* Stream encoding is not supported yet.
*
* @author Sebastien Deleuze
* @since 7.0
* @see JacksonCborDecoder
* @see <a href="https://github.com/spring-projects/spring-framework/issues/20513">Add CBOR support to WebFlux</a>
*/
public class JacksonCborEncoder extends AbstractJacksonEncoder {
/**
* Construct a new instance with a {@link CBORMapper} customized with the
* {@link tools.jackson.databind.JacksonModule}s found by
* {@link MapperBuilder#findModules(ClassLoader)}.
*/
public JacksonCborEncoder() {
super(CBORMapper.builder(), MediaType.APPLICATION_CBOR);
}
/**
* Construct a new instance with the provided {@link CBORMapper}.
* @see CBORMapper#builder()
* @see MapperBuilder#findAndAddModules(ClassLoader)
*/
public JacksonCborEncoder(CBORMapper mapper) {
super(mapper, MediaType.APPLICATION_CBOR);
}
/**
* Construct a new instance with the provided {@link CBORMapper} and {@link MimeType}s.
* @see CBORMapper#builder()
* @see MapperBuilder#findAndAddModules(ClassLoader)
*/
public JacksonCborEncoder(CBORMapper mapper, MimeType... mimeTypes) {
super(mapper, mimeTypes);
}
@Override
public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory, ResolvableType elementType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
throw new UnsupportedOperationException("Does not support stream encoding yet");
}
}

View File

@@ -44,7 +44,7 @@ import org.springframework.core.io.buffer.DataBufferUtils;
/**
* {@link Function} to transform a JSON stream of arbitrary size, byte array
* chunks into a {@code Flux<TokenBuffer>} where each token buffer is a
* well-formed JSON object.
* well-formed JSON object with Jackson 2.x.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev

View File

@@ -0,0 +1,114 @@
/*
* 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.codec.json;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.cfg.MapperBuilder;
import tools.jackson.databind.json.JsonMapper;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.CharBufferDecoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.codec.AbstractJacksonDecoder;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
/**
* Decode a byte stream into JSON and convert to Object's with
* <a href="https://github.com/FasterXML/jackson">Jackson 3.x</a>
* leveraging non-blocking parsing.
*
* <p>The default constructor loads {@link tools.jackson.databind.JacksonModule}s
* found by {@link MapperBuilder#findModules(ClassLoader)}.
*
* @author Sebastien Deleuze
* @since 7.0
* @see JacksonJsonEncoder
*/
public class JacksonJsonDecoder extends AbstractJacksonDecoder {
private static final CharBufferDecoder CHAR_BUFFER_DECODER = CharBufferDecoder.textPlainOnly(Arrays.asList(",", "\n"), false);
private static final ResolvableType CHAR_BUFFER_TYPE = ResolvableType.forClass(CharBuffer.class);
private static final MimeType[] DEFAULT_JSON_MIME_TYPES = new MimeType[] {
MediaType.APPLICATION_JSON,
new MediaType("application", "*+json"),
MediaType.APPLICATION_NDJSON
};
/**
* Construct a new instance with a {@link JsonMapper} customized with the
* {@link tools.jackson.databind.JacksonModule}s found by
* {@link MapperBuilder#findModules(ClassLoader)}.
*/
public JacksonJsonDecoder() {
super(JsonMapper.builder(), DEFAULT_JSON_MIME_TYPES);
}
/**
* Construct a new instance with the provided {@link ObjectMapper}.
* @see JsonMapper#builder()
* @see MapperBuilder#findModules(ClassLoader)
*/
public JacksonJsonDecoder(ObjectMapper mapper) {
this(mapper, DEFAULT_JSON_MIME_TYPES);
}
/**
* Construct a new instance with the provided {@link ObjectMapper} and {@link MimeType}s.
* @see JsonMapper#builder()
* @see MapperBuilder#findModules(ClassLoader)
*/
public JacksonJsonDecoder(ObjectMapper mapper, MimeType... mimeTypes) {
super(mapper, mimeTypes);
}
@Override
protected Flux<DataBuffer> processInput(Publisher<DataBuffer> input, ResolvableType elementType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
Flux<DataBuffer> flux = Flux.from(input);
if (mimeType == null) {
return flux;
}
// Jackson asynchronous parser only supports UTF-8
Charset charset = mimeType.getCharset();
if (charset == null || StandardCharsets.UTF_8.equals(charset) || StandardCharsets.US_ASCII.equals(charset)) {
return flux;
}
// Re-encode as UTF-8.
MimeType textMimeType = new MimeType(MimeTypeUtils.TEXT_PLAIN, charset);
Flux<CharBuffer> decoded = CHAR_BUFFER_DECODER.decode(input, CHAR_BUFFER_TYPE, textMimeType, null);
return decoded.map(charBuffer -> DefaultDataBufferFactory.sharedInstance.wrap(StandardCharsets.UTF_8.encode(charBuffer)));
}
}

View File

@@ -0,0 +1,125 @@
/*
* 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.codec.json;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Flux;
import tools.jackson.core.PrettyPrinter;
import tools.jackson.core.util.DefaultIndenter;
import tools.jackson.core.util.DefaultPrettyPrinter;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectWriter;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.cfg.MapperBuilder;
import tools.jackson.databind.json.JsonMapper;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.http.ProblemDetail;
import org.springframework.http.codec.AbstractJacksonEncoder;
import org.springframework.http.converter.json.ProblemDetailJacksonMixin;
import org.springframework.util.MimeType;
/**
* Encode from an {@code Object} stream to a byte stream of JSON objects using
* <a href="https://github.com/FasterXML/jackson">Jackson 3.x</a>. For non-streaming
* use cases, {@link Flux} elements are collected into a {@link List} before
* serialization for performance reason.
*
* <p>The default constructor loads {@link tools.jackson.databind.JacksonModule}s
* found by {@link MapperBuilder#findModules(ClassLoader)}.
*
* @author Sebastien Deleuze
* @since 7.0
* @see JacksonJsonDecoder
*/
public class JacksonJsonEncoder extends AbstractJacksonEncoder {
private static final List<MimeType> problemDetailMimeTypes =
Collections.singletonList(MediaType.APPLICATION_PROBLEM_JSON);
private static final MimeType[] DEFAULT_JSON_MIME_TYPES = new MimeType[] {
MediaType.APPLICATION_JSON,
new MediaType("application", "*+json"),
MediaType.APPLICATION_NDJSON
};
private final @Nullable PrettyPrinter ssePrettyPrinter;
/**
* 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 JacksonJsonEncoder() {
super(JsonMapper.builder().addMixIn(ProblemDetail.class, ProblemDetailJacksonMixin.class),
DEFAULT_JSON_MIME_TYPES);
setStreamingMediaTypes(List.of(MediaType.APPLICATION_NDJSON));
this.ssePrettyPrinter = initSsePrettyPrinter();
}
/**
* Construct a new instance with the provided {@link ObjectMapper}.
* @see JsonMapper#builder()
* @see MapperBuilder#findModules(ClassLoader)
*/
public JacksonJsonEncoder(ObjectMapper mapper) {
this(mapper, DEFAULT_JSON_MIME_TYPES);
}
/**
* Construct a new instance with the provided {@link ObjectMapper} and
* {@link MimeType}s.
* @see JsonMapper#builder()
* @see MapperBuilder#findModules(ClassLoader)
*/
public JacksonJsonEncoder(ObjectMapper mapper, MimeType... mimeTypes) {
super(mapper, mimeTypes);
setStreamingMediaTypes(List.of(MediaType.APPLICATION_NDJSON));
this.ssePrettyPrinter = initSsePrettyPrinter();
}
private static PrettyPrinter initSsePrettyPrinter() {
DefaultPrettyPrinter printer = new DefaultPrettyPrinter();
printer.indentObjectsWith(new DefaultIndenter(" ", "\ndata:"));
return printer;
}
@Override
protected List<MimeType> getMediaTypesForProblemDetail() {
return problemDetailMimeTypes;
}
@Override
protected ObjectWriter customizeWriter(ObjectWriter writer, @Nullable MimeType mimeType,
ResolvableType elementType, @Nullable Map<String, Object> hints) {
return (this.ssePrettyPrinter != null &&
MediaType.TEXT_EVENT_STREAM.isCompatibleWith(mimeType) &&
writer.getConfig().isEnabled(SerializationFeature.INDENT_OUTPUT) ?
writer.with(this.ssePrettyPrinter) : writer);
}
}

View File

@@ -0,0 +1,69 @@
/*
* 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.codec.smile;
import tools.jackson.databind.cfg.MapperBuilder;
import tools.jackson.dataformat.smile.SmileMapper;
import org.springframework.http.codec.AbstractJacksonDecoder;
import org.springframework.util.MimeType;
/**
* Decode a byte stream into Smile and convert to Object's with Jackson 3.x,
* leveraging non-blocking parsing.
*
* <p>The default constructor loads {@link tools.jackson.databind.JacksonModule}s
* found by {@link MapperBuilder#findModules(ClassLoader)}.
*
* @author Sebastien Deleuze
* @since 7.0
* @see JacksonSmileEncoder
*/
public class JacksonSmileDecoder extends AbstractJacksonDecoder {
private static final MimeType[] DEFAULT_SMILE_MIME_TYPES = new MimeType[] {
new MimeType("application", "x-jackson-smile"),
new MimeType("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 JacksonSmileDecoder() {
super(SmileMapper.builder(), DEFAULT_SMILE_MIME_TYPES);
}
/**
* Construct a new instance with the provided {@link SmileMapper}.
* @see SmileMapper#builder()
* @see MapperBuilder#findAndAddModules(ClassLoader)
*/
public JacksonSmileDecoder(SmileMapper mapper) {
this(mapper, DEFAULT_SMILE_MIME_TYPES);
}
/**
* Construct a new instance with the provided {@link SmileMapper} and {@link MimeType}s.
* @see SmileMapper#builder()
* @see MapperBuilder#findAndAddModules(ClassLoader)
*/
public JacksonSmileDecoder(SmileMapper mapper, MimeType... mimeTypes) {
super(mapper, mimeTypes);
}
}

View File

@@ -0,0 +1,101 @@
/*
* 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.codec.smile;
import java.util.Collections;
import java.util.List;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Flux;
import tools.jackson.databind.cfg.MapperBuilder;
import tools.jackson.dataformat.smile.SmileMapper;
import org.springframework.http.MediaType;
import org.springframework.http.codec.AbstractJacksonEncoder;
import org.springframework.util.MimeType;
/**
* Encode from an {@code Object} stream to a byte stream of Smile objects using Jackson 3.x.
* For non-streaming use cases, {@link Flux} elements are collected into a {@link List}
* before serialization for performance reason.
*
* <p>The default constructor loads {@link tools.jackson.databind.JacksonModule}s
* found by {@link MapperBuilder#findModules(ClassLoader)}.
*
* @author Sebastien Deleuze
* @since 7.0
* @see JacksonSmileDecoder
*/
public class JacksonSmileEncoder extends AbstractJacksonEncoder {
private static final MimeType[] DEFAULT_SMILE_MIME_TYPES = new MimeType[] {
new MimeType("application", "x-jackson-smile"),
new MimeType("application", "*+x-jackson-smile")};
private static final MediaType DEFAULT_SMILE_STREAMING_MEDIA_TYPE =
new MediaType("application", "stream+x-jackson-smile");
private static final byte[] STREAM_SEPARATOR = new byte[0];
/**
* Construct a new instance with a {@link SmileMapper} customized with the
* {@link tools.jackson.databind.JacksonModule}s found by
* {@link MapperBuilder#findModules(ClassLoader)}.
*/
public JacksonSmileEncoder() {
super(SmileMapper.builder(), DEFAULT_SMILE_MIME_TYPES);
setStreamingMediaTypes(Collections.singletonList(DEFAULT_SMILE_STREAMING_MEDIA_TYPE));
}
/**
* Construct a new instance with the provided {@link SmileMapper}.
* @see SmileMapper#builder()
* @see MapperBuilder#findAndAddModules(ClassLoader)
*/
public JacksonSmileEncoder(SmileMapper mapper) {
super(mapper, DEFAULT_SMILE_MIME_TYPES);
setStreamingMediaTypes(Collections.singletonList(DEFAULT_SMILE_STREAMING_MEDIA_TYPE));
}
/**
* Construct a new instance with the provided {@link SmileMapper} and {@link MimeType}s.
* @see SmileMapper#builder()
* @see MapperBuilder#findAndAddModules(ClassLoader)
*/
public JacksonSmileEncoder(SmileMapper mapper, MimeType... mimeTypes) {
super(mapper, mimeTypes);
setStreamingMediaTypes(Collections.singletonList(DEFAULT_SMILE_STREAMING_MEDIA_TYPE));
}
/**
* Return the separator to use for the given mime type.
* <p>By default, this method returns a single byte 0 if the given
* mime type is one of the configured {@link #setStreamingMediaTypes(List)
* streaming} mime types.
*/
@Override
protected byte @Nullable [] getStreamingMediaTypeSeparator(@Nullable MimeType mimeType) {
for (MediaType streamingMediaType : getStreamingMediaTypes()) {
if (streamingMediaType.isCompatibleWith(mimeType)) {
return STREAM_SEPARATOR;
}
}
return null;
}
}

View File

@@ -0,0 +1,7 @@
/**
* Provides an encoder and a decoder for the Smile data format ("binary JSON").
*/
@NullMarked
package org.springframework.http.codec.smile;
import org.jspecify.annotations.NullMarked;

View File

@@ -39,6 +39,7 @@ import org.springframework.core.codec.NettyByteBufDecoder;
import org.springframework.core.codec.NettyByteBufEncoder;
import org.springframework.core.codec.ResourceDecoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.http.codec.AbstractJacksonDecoder;
import org.springframework.http.codec.CodecConfigurer;
import org.springframework.http.codec.DecoderHttpMessageReader;
import org.springframework.http.codec.EncoderHttpMessageWriter;
@@ -57,6 +58,8 @@ import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.codec.json.Jackson2SmileDecoder;
import org.springframework.http.codec.json.Jackson2SmileEncoder;
import org.springframework.http.codec.json.JacksonJsonDecoder;
import org.springframework.http.codec.json.JacksonJsonEncoder;
import org.springframework.http.codec.json.KotlinSerializationJsonDecoder;
import org.springframework.http.codec.json.KotlinSerializationJsonEncoder;
import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader;
@@ -70,6 +73,8 @@ import org.springframework.http.codec.protobuf.KotlinSerializationProtobufEncode
import org.springframework.http.codec.protobuf.ProtobufDecoder;
import org.springframework.http.codec.protobuf.ProtobufEncoder;
import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter;
import org.springframework.http.codec.smile.JacksonSmileDecoder;
import org.springframework.http.codec.smile.JacksonSmileEncoder;
import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
import org.springframework.util.ClassUtils;
@@ -84,8 +89,12 @@ import org.springframework.util.ObjectUtils;
*/
class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigurer.DefaultCodecConfig {
static final boolean jacksonPresent;
static final boolean jackson2Present;
private static final boolean jacksonSmilePresent;
private static final boolean jackson2SmilePresent;
private static final boolean jaxb2Present;
@@ -102,9 +111,11 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
static {
ClassLoader classLoader = BaseCodecConfigurer.class.getClassLoader();
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);
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
jacksonSmilePresent = jacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.smile.SmileMapper", classLoader);
jackson2SmilePresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
jaxb2Present = ClassUtils.isPresent("jakarta.xml.bind.Binder", classLoader);
protobufPresent = ClassUtils.isPresent("com.google.protobuf.Message", classLoader);
nettyByteBufPresent = ClassUtils.isPresent("io.netty.buffer.ByteBuf", classLoader);
@@ -114,12 +125,20 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
}
private @Nullable Decoder<?> jacksonJsonDecoder;
private @Nullable Decoder<?> jackson2JsonDecoder;
private @Nullable Encoder<?> jacksonJsonEncoder;
private @Nullable Encoder<?> jackson2JsonEncoder;
private @Nullable Encoder<?> jacksonSmileEncoder;
private @Nullable Encoder<?> jackson2SmileEncoder;
private @Nullable Decoder<?> jacksonSmileDecoder;
private @Nullable Decoder<?> jackson2SmileDecoder;
private @Nullable Decoder<?> protobufDecoder;
@@ -195,9 +214,13 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
* Create a deep copy of the given {@link BaseDefaultCodecs}.
*/
protected BaseDefaultCodecs(BaseDefaultCodecs other) {
this.jacksonJsonDecoder = other.jacksonJsonDecoder;
this.jackson2JsonDecoder = other.jackson2JsonDecoder;
this.jacksonJsonEncoder = other.jacksonJsonEncoder;
this.jackson2JsonEncoder = other.jackson2JsonEncoder;
this.jacksonSmileDecoder = other.jacksonSmileDecoder;
this.jackson2SmileDecoder = other.jackson2SmileDecoder;
this.jacksonSmileEncoder = other.jacksonSmileEncoder;
this.jackson2SmileEncoder = other.jackson2SmileEncoder;
this.protobufDecoder = other.protobufDecoder;
this.protobufEncoder = other.protobufEncoder;
@@ -222,12 +245,25 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
this.objectWriters.addAll(other.objectWriters);
}
@Override
public void jacksonJsonDecoder(Decoder<?> decoder) {
this.jacksonJsonDecoder = decoder;
initObjectReaders();
}
@Override
public void jackson2JsonDecoder(Decoder<?> decoder) {
this.jackson2JsonDecoder = decoder;
initObjectReaders();
}
@Override
public void jacksonJsonEncoder(Encoder<?> encoder) {
this.jacksonJsonEncoder = encoder;
initObjectWriters();
initTypedWriters();
}
@Override
public void jackson2JsonEncoder(Encoder<?> encoder) {
this.jackson2JsonEncoder = encoder;
@@ -235,12 +271,25 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
initTypedWriters();
}
@Override
public void jacksonSmileDecoder(Decoder<?> decoder) {
this.jacksonSmileDecoder = decoder;
initObjectReaders();
}
@Override
public void jackson2SmileDecoder(Decoder<?> decoder) {
this.jackson2SmileDecoder = decoder;
initObjectReaders();
}
@Override
public void jacksonSmileEncoder(Encoder<?> encoder) {
this.jacksonSmileEncoder = encoder;
initObjectWriters();
initTypedWriters();
}
@Override
public void jackson2SmileEncoder(Encoder<?> encoder) {
this.jackson2SmileEncoder = encoder;
@@ -476,6 +525,11 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
kotlinSerializationProtobufDec.setMaxInMemorySize(size);
}
}
if (jacksonPresent) {
if (codec instanceof AbstractJacksonDecoder abstractJacksonDecoder) {
abstractJacksonDecoder.setMaxInMemorySize(size);
}
}
if (jackson2Present) {
if (codec instanceof AbstractJackson2Decoder abstractJackson2Decoder) {
abstractJackson2Decoder.setMaxInMemorySize(size);
@@ -574,13 +628,20 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
(KotlinSerializationProtobufDecoder) this.kotlinSerializationProtobufDecoder :
new KotlinSerializationProtobufDecoder()));
}
if (jackson2Present) {
if (jacksonPresent) {
addCodec(this.objectReaders, new DecoderHttpMessageReader<>(getJacksonJsonDecoder()));
}
else if (jackson2Present) {
addCodec(this.objectReaders, new DecoderHttpMessageReader<>(getJackson2JsonDecoder()));
}
else if (kotlinSerializationJsonPresent) {
addCodec(this.objectReaders, new DecoderHttpMessageReader<>(getKotlinSerializationJsonDecoder()));
}
if (jackson2SmilePresent) {
if (jacksonSmilePresent) {
addCodec(this.objectReaders, new DecoderHttpMessageReader<>(this.jacksonSmileDecoder != null ?
(JacksonSmileDecoder) this.jacksonSmileDecoder : new JacksonSmileDecoder()));
}
else if (jackson2SmilePresent) {
addCodec(this.objectReaders, new DecoderHttpMessageReader<>(this.jackson2SmileDecoder != null ?
(Jackson2SmileDecoder) this.jackson2SmileDecoder : new Jackson2SmileDecoder()));
}
@@ -711,13 +772,20 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
(KotlinSerializationProtobufEncoder) this.kotlinSerializationProtobufEncoder :
new KotlinSerializationProtobufEncoder()));
}
if (jackson2Present) {
if (jacksonPresent) {
addCodec(writers, new EncoderHttpMessageWriter<>(getJacksonJsonEncoder()));
}
else if (jackson2Present) {
addCodec(writers, new EncoderHttpMessageWriter<>(getJackson2JsonEncoder()));
}
else if (kotlinSerializationJsonPresent) {
addCodec(writers, new EncoderHttpMessageWriter<>(getKotlinSerializationJsonEncoder()));
}
if (jackson2SmilePresent) {
if (jacksonSmilePresent) {
addCodec(writers, new EncoderHttpMessageWriter<>(this.jacksonSmileEncoder != null ?
(JacksonSmileEncoder) this.jacksonSmileEncoder : new JacksonSmileEncoder()));
}
else if (jackson2SmilePresent) {
addCodec(writers, new EncoderHttpMessageWriter<>(this.jackson2SmileEncoder != null ?
(Jackson2SmileEncoder) this.jackson2SmileEncoder : new Jackson2SmileEncoder()));
}
@@ -764,6 +832,13 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
// Accessors for use in subclasses...
protected Decoder<?> getJacksonJsonDecoder() {
if (this.jacksonJsonDecoder == null) {
this.jacksonJsonDecoder = new JacksonJsonDecoder();
}
return this.jacksonJsonDecoder;
}
protected Decoder<?> getJackson2JsonDecoder() {
if (this.jackson2JsonDecoder == null) {
this.jackson2JsonDecoder = new Jackson2JsonDecoder();
@@ -771,6 +846,13 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
return this.jackson2JsonDecoder;
}
protected Encoder<?> getJacksonJsonEncoder() {
if (this.jacksonJsonEncoder == null) {
this.jacksonJsonEncoder = new JacksonJsonEncoder();
}
return this.jacksonJsonEncoder;
}
protected Encoder<?> getJackson2JsonEncoder() {
if (this.jackson2JsonEncoder == null) {
this.jackson2JsonEncoder = new Jackson2JsonEncoder();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -53,6 +53,7 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo
protected void extendObjectReaders(List<HttpMessageReader<?>> objectReaders) {
Decoder<?> decoder = (this.sseDecoder != null ? this.sseDecoder :
jacksonPresent ? getJacksonJsonDecoder() :
jackson2Present ? getJackson2JsonDecoder() :
kotlinSerializationJsonPresent ? getKotlinSerializationJsonDecoder() :
null);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
@@ -57,6 +57,7 @@ class ServerDefaultCodecsImpl extends BaseDefaultCodecs implements ServerCodecCo
private @Nullable Encoder<?> getSseEncoder() {
return this.sseEncoder != null ? this.sseEncoder :
jacksonPresent ? getJacksonJsonEncoder() :
jackson2Present ? getJackson2JsonEncoder() :
kotlinSerializationJsonPresent ? getKotlinSerializationJsonEncoder() :
null;