Configure Kotlin JSON converters as Jackson alternative

Prior to this commit, Spring MVC and WebFlux would consider the
"kotlinx.serialization" JSON codecs and converters in addition to other
JSON alternatives like Jackson, Gson and Jsonb.

This would cause issues because while in most cases this library is only
involved if the type is annotated with "@Serializable", this is not true
for Java enums. In this particular case, the codec shadows Jackson and
causes issues.

This commit now considers kotlinx.serialization JSON support as an
alternative to Jackson. Just like Jsonb and GSON, this is only
auto-detected if Jackson is not present.
We received consistent feedback that kotlinx.serialization is popular in
Kotlin libraries and is often a transitive dependency. As a result, we
cannot consider its presence on the classpath as a strong enough signal
to configure it by default.

Closes gh-34410
This commit is contained in:
Brian Clozel
2025-02-11 20:05:53 +01:00
parent 6ded25ba28
commit 7077809561
8 changed files with 30 additions and 41 deletions

View File

@@ -54,6 +54,11 @@ You can customize XML mapping as needed through the use of JAXB or Jackson's pro
When you need further control (for cases where custom XML serializers/deserializers need to be provided for specific types), you can inject a custom `XmlMapper` through the `ObjectMapper` property.
By default, this converter supports `application/xml`. This requires the `com.fasterxml.jackson.dataformat:jackson-dataformat-xml` dependency.
| `KotlinSerializationJsonHttpMessageConverter`
| An `HttpMessageConverter` implementation that can read and write JSON using `kotlinx.serialization`.
This converter is not configured by default, as this conflicts with Jackson.
Developers must configure it as an additional converter ahead of the Jackson one.
| `MappingJackson2CborHttpMessageConverter`
| `com.fasterxml.jackson.dataformat:jackson-dataformat-cbor`

View File

@@ -575,9 +575,6 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
(KotlinSerializationCborDecoder) this.kotlinSerializationCborDecoder :
new KotlinSerializationCborDecoder()));
}
if (kotlinSerializationJsonPresent) {
addCodec(this.objectReaders, new DecoderHttpMessageReader<>(getKotlinSerializationJsonDecoder()));
}
if (kotlinSerializationProtobufPresent) {
addCodec(this.objectReaders,
new DecoderHttpMessageReader<>(this.kotlinSerializationProtobufDecoder != null ?
@@ -587,6 +584,9 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
if (jackson2Present) {
addCodec(this.objectReaders, new DecoderHttpMessageReader<>(getJackson2JsonDecoder()));
}
else if (kotlinSerializationJsonPresent) {
addCodec(this.objectReaders, new DecoderHttpMessageReader<>(getKotlinSerializationJsonDecoder()));
}
if (jackson2SmilePresent) {
addCodec(this.objectReaders, new DecoderHttpMessageReader<>(this.jackson2SmileDecoder != null ?
(Jackson2SmileDecoder) this.jackson2SmileDecoder : new Jackson2SmileDecoder()));
@@ -713,9 +713,6 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
(KotlinSerializationCborEncoder) this.kotlinSerializationCborEncoder :
new KotlinSerializationCborEncoder()));
}
if (kotlinSerializationJsonPresent) {
addCodec(writers, new EncoderHttpMessageWriter<>(getKotlinSerializationJsonEncoder()));
}
if (kotlinSerializationProtobufPresent) {
addCodec(writers, new EncoderHttpMessageWriter<>(this.kotlinSerializationProtobufEncoder != null ?
(KotlinSerializationProtobufEncoder) this.kotlinSerializationProtobufEncoder :
@@ -724,6 +721,9 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
if (jackson2Present) {
addCodec(writers, new EncoderHttpMessageWriter<>(getJackson2JsonEncoder()));
}
else if (kotlinSerializationJsonPresent) {
addCodec(writers, new EncoderHttpMessageWriter<>(getKotlinSerializationJsonEncoder()));
}
if (jackson2SmilePresent) {
addCodec(writers, new EncoderHttpMessageWriter<>(this.jackson2SmileEncoder != null ?
(Jackson2SmileEncoder) this.jackson2SmileEncoder : new Jackson2SmileEncoder()));

View File

@@ -60,8 +60,6 @@ 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.KotlinSerializationJsonDecoder;
import org.springframework.http.codec.json.KotlinSerializationJsonEncoder;
import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader;
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
import org.springframework.http.codec.multipart.MultipartHttpMessageWriter;
@@ -94,7 +92,7 @@ class ClientCodecConfigurerTests {
@Test
void defaultReaders() {
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
assertThat(readers).hasSize(19);
assertThat(readers).hasSize(18);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteArrayDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteBufferDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(DataBufferDecoder.class);
@@ -108,7 +106,6 @@ class ClientCodecConfigurerTests {
assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(MultipartHttpMessageReader.class);
assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartEventHttpMessageReader.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationCborDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationJsonDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationProtobufDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class);
@@ -120,7 +117,7 @@ class ClientCodecConfigurerTests {
@Test
void defaultWriters() {
List<HttpMessageWriter<?>> writers = this.configurer.getWriters();
assertThat(writers).hasSize(17);
assertThat(writers).hasSize(16);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteArrayEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteBufferEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(DataBufferEncoder.class);
@@ -132,7 +129,6 @@ class ClientCodecConfigurerTests {
assertThat(writers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartEventHttpMessageWriter.class);
assertThat(writers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartHttpMessageWriter.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationCborEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationJsonEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationProtobufEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class);
@@ -188,7 +184,7 @@ class ClientCodecConfigurerTests {
int size = 99;
this.configurer.defaultCodecs().maxInMemorySize(size);
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
assertThat(readers).hasSize(19);
assertThat(readers).hasSize(18);
assertThat(((ByteArrayDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
assertThat(((ByteBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
assertThat(((DataBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
@@ -201,7 +197,6 @@ class ClientCodecConfigurerTests {
nextReader(readers);
assertThat(((PartEventHttpMessageReader) nextReader(readers)).getMaxInMemorySize()).isEqualTo(size);
assertThat(((KotlinSerializationCborDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
assertThat(((KotlinSerializationJsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
assertThat(((KotlinSerializationProtobufDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
assertThat(((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
assertThat(((Jackson2SmileDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
@@ -250,7 +245,7 @@ class ClientCodecConfigurerTests {
writers = findCodec(this.configurer.getWriters(), MultipartHttpMessageWriter.class).getPartWriters();
assertThat(sseDecoder).isNotSameAs(jackson2Decoder);
assertThat(writers).hasSize(17);
assertThat(writers).hasSize(16);
}
@Test // gh-24194
@@ -260,7 +255,7 @@ class ClientCodecConfigurerTests {
List<HttpMessageWriter<?>> writers =
findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters();
assertThat(writers).hasSize(17);
assertThat(writers).hasSize(16);
}
@Test
@@ -274,7 +269,7 @@ class ClientCodecConfigurerTests {
List<HttpMessageWriter<?>> writers =
findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters();
assertThat(writers).hasSize(17);
assertThat(writers).hasSize(16);
}
private Decoder<?> getNextDecoder(List<HttpMessageReader<?>> readers) {

View File

@@ -53,8 +53,6 @@ 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.KotlinSerializationJsonDecoder;
import org.springframework.http.codec.json.KotlinSerializationJsonEncoder;
import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader;
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
import org.springframework.http.codec.multipart.MultipartHttpMessageWriter;
@@ -90,7 +88,7 @@ class CodecConfigurerTests {
@Test
void defaultReaders() {
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
assertThat(readers).hasSize(18);
assertThat(readers).hasSize(17);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteArrayDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteBufferDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(DataBufferDecoder.class);
@@ -103,7 +101,6 @@ class CodecConfigurerTests {
assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(MultipartHttpMessageReader.class);
assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartEventHttpMessageReader.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationCborDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationJsonDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationProtobufDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class);
@@ -114,7 +111,7 @@ class CodecConfigurerTests {
@Test
void defaultWriters() {
List<HttpMessageWriter<?>> writers = this.configurer.getWriters();
assertThat(writers).hasSize(17);
assertThat(writers).hasSize(16);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteArrayEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteBufferEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(DataBufferEncoder.class);
@@ -126,7 +123,6 @@ class CodecConfigurerTests {
assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(PartEventHttpMessageWriter.class);
assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(PartHttpMessageWriter.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationCborEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationJsonEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationProtobufEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class);
@@ -156,7 +152,7 @@ class CodecConfigurerTests {
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
assertThat(readers).hasSize(22);
assertThat(readers).hasSize(21);
assertThat(getNextDecoder(readers)).isSameAs(customDecoder1);
assertThat(readers.get(this.index.getAndIncrement())).isSameAs(customReader1);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteArrayDecoder.class);
@@ -173,7 +169,6 @@ class CodecConfigurerTests {
assertThat(getNextDecoder(readers)).isSameAs(customDecoder2);
assertThat(readers.get(this.index.getAndIncrement())).isSameAs(customReader2);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationCborDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationJsonDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationProtobufDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class);
@@ -203,7 +198,7 @@ class CodecConfigurerTests {
List<HttpMessageWriter<?>> writers = this.configurer.getWriters();
assertThat(writers).hasSize(21);
assertThat(writers).hasSize(20);
assertThat(getNextEncoder(writers)).isSameAs(customEncoder1);
assertThat(writers.get(this.index.getAndIncrement())).isSameAs(customWriter1);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteArrayEncoder.class);
@@ -219,7 +214,6 @@ class CodecConfigurerTests {
assertThat(getNextEncoder(writers)).isSameAs(customEncoder2);
assertThat(writers.get(this.index.getAndIncrement())).isSameAs(customWriter2);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationCborEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationJsonEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationProtobufEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class);

View File

@@ -58,8 +58,6 @@ 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.KotlinSerializationJsonDecoder;
import org.springframework.http.codec.json.KotlinSerializationJsonEncoder;
import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader;
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
import org.springframework.http.codec.multipart.MultipartHttpMessageWriter;
@@ -92,7 +90,7 @@ class ServerCodecConfigurerTests {
@Test
void defaultReaders() {
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
assertThat(readers).hasSize(18);
assertThat(readers).hasSize(17);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteArrayDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteBufferDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(DataBufferDecoder.class);
@@ -105,7 +103,6 @@ class ServerCodecConfigurerTests {
assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(MultipartHttpMessageReader.class);
assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartEventHttpMessageReader.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationCborDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationJsonDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationProtobufDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class);
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class);
@@ -116,7 +113,7 @@ class ServerCodecConfigurerTests {
@Test
void defaultWriters() {
List<HttpMessageWriter<?>> writers = this.configurer.getWriters();
assertThat(writers).hasSize(18);
assertThat(writers).hasSize(17);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteArrayEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteBufferEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(DataBufferEncoder.class);
@@ -128,7 +125,6 @@ class ServerCodecConfigurerTests {
assertThat(writers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartEventHttpMessageWriter.class);
assertThat(writers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartHttpMessageWriter.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationCborEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationJsonEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationProtobufEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class);
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class);
@@ -176,7 +172,6 @@ class ServerCodecConfigurerTests {
assertThat(((PartEventHttpMessageReader) nextReader(readers)).getMaxInMemorySize()).isEqualTo(size);
assertThat(((KotlinSerializationCborDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
assertThat(((KotlinSerializationJsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
assertThat(((KotlinSerializationProtobufDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
assertThat(((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
assertThat(((Jackson2SmileDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);

View File

@@ -116,7 +116,7 @@ public class DelegatingWebFluxConfigurationTests {
boolean condition = initializer.getValidator() instanceof LocalValidatorFactoryBean;
assertThat(condition).isTrue();
assertThat(initializer.getConversionService()).isSameAs(formatterRegistry.getValue());
assertThat(codecsConfigurer.getValue().getReaders()).hasSize(16);
assertThat(codecsConfigurer.getValue().getReaders()).hasSize(15);
}
@Test

View File

@@ -114,7 +114,7 @@ class WebFluxConfigurationSupportTests {
assertThat(adapter).isNotNull();
List<HttpMessageReader<?>> readers = adapter.getMessageReaders();
assertThat(readers).hasSize(16);
assertThat(readers).hasSize(15);
ResolvableType multiValueMapType = forClassWithGenerics(MultiValueMap.class, String.class, String.class);
@@ -169,7 +169,7 @@ class WebFluxConfigurationSupportTests {
assertThat(handler.getOrder()).isEqualTo(0);
List<HttpMessageWriter<?>> writers = handler.getMessageWriters();
assertThat(writers).hasSize(16);
assertThat(writers).hasSize(15);
assertHasMessageWriter(writers, forClass(byte[].class), APPLICATION_OCTET_STREAM);
assertHasMessageWriter(writers, forClass(ByteBuffer.class), APPLICATION_OCTET_STREAM);
@@ -197,7 +197,7 @@ class WebFluxConfigurationSupportTests {
assertThat(handler.getOrder()).isEqualTo(100);
List<HttpMessageWriter<?>> writers = handler.getMessageWriters();
assertThat(writers).hasSize(16);
assertThat(writers).hasSize(15);
assertHasMessageWriter(writers, forClass(byte[].class), APPLICATION_OCTET_STREAM);
assertHasMessageWriter(writers, forClass(ByteBuffer.class), APPLICATION_OCTET_STREAM);

View File

@@ -894,9 +894,6 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
if (kotlinSerializationCborPresent) {
messageConverters.add(new KotlinSerializationCborHttpMessageConverter());
}
if (kotlinSerializationJsonPresent) {
messageConverters.add(new KotlinSerializationJsonHttpMessageConverter());
}
if (kotlinSerializationProtobufPresent) {
messageConverters.add(new KotlinSerializationProtobufHttpMessageConverter());
}
@@ -914,6 +911,9 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
else if (jsonbPresent) {
messageConverters.add(new JsonbHttpMessageConverter());
}
else if (kotlinSerializationJsonPresent) {
messageConverters.add(new KotlinSerializationJsonHttpMessageConverter());
}
if (jackson2SmilePresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile();