Restrict Kotlin serialization when alternative is available

This commit applies similar changes already contributed to the
`HttpMessageConverter` stack for MVC applications.
Since there was no auto-configuration for Kotlinx JSON Serialization on
the reactive side, this commit adds a relevant `CodecCustomizer` that
will use an available `Json` bean and use it to configure a codec that:

* will only consider `@Serializable`-annotated types if another JSON
  library is available
* will use a broader support with Kotlinx Serialization otherwise

Fixes gh-48070
This commit is contained in:
Brian Clozel
2025-11-12 21:09:39 +01:00
parent eb381d642a
commit d161aafcdf
3 changed files with 80 additions and 0 deletions
@@ -32,6 +32,7 @@ dependencies {
optional(project(":core:spring-boot-test"))
optional(project(":module:spring-boot-jackson"))
optional(project(":module:spring-boot-jackson2"))
optional(project(":module:spring-boot-kotlinx-serialization-json"))
optional("org.springframework:spring-webflux")
testImplementation(project(":core:spring-boot-test"))
@@ -17,6 +17,7 @@
package org.springframework.boot.http.codec.autoconfigure;
import com.fasterxml.jackson.databind.ObjectMapper;
import kotlinx.serialization.json.Json;
import org.jspecify.annotations.Nullable;
import tools.jackson.databind.json.JsonMapper;
@@ -35,9 +36,13 @@ import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ResourceLoader;
import org.springframework.http.codec.CodecConfigurer;
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.util.ClassUtils;
import org.springframework.util.unit.DataSize;
import org.springframework.web.reactive.function.client.WebClient;
@@ -93,6 +98,29 @@ public final class CodecsAutoConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Json.class)
static class KotlinxSerializationJsonCodecConfiguration {
@Bean
@ConditionalOnBean(Json.class)
CodecCustomizer kotlinxJsonCodecCustomizer(Json json, ResourceLoader resourceLoader) {
ClassLoader classLoader = resourceLoader.getClassLoader();
boolean hasAnyJsonSupport = ClassUtils.isPresent("tools.jackson.databind.json.JsonMapper", classLoader)
|| ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
|| ClassUtils.isPresent("com.google.gson.Gson", classLoader);
return (configurer) -> {
CodecConfigurer.DefaultCodecs defaults = configurer.defaultCodecs();
defaults.kotlinSerializationJsonEncoder(hasAnyJsonSupport ? new KotlinSerializationJsonEncoder(json)
: new KotlinSerializationJsonEncoder(json, (type) -> true));
defaults.kotlinSerializationJsonDecoder(hasAnyJsonSupport ? new KotlinSerializationJsonDecoder(json)
: new KotlinSerializationJsonDecoder(json, (type) -> true));
};
}
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(HttpCodecsProperties.class)
static class DefaultCodecsConfiguration {
@@ -17,8 +17,10 @@
package org.springframework.boot.http.codec.autoconfigure;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import kotlinx.serialization.json.Json;
import org.junit.jupiter.api.Test;
import tools.jackson.databind.json.JsonMapper;
@@ -30,8 +32,13 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.http.codec.CodecConfigurer;
import org.springframework.http.codec.CodecConfigurer.DefaultCodecs;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.codec.json.KotlinSerializationJsonEncoder;
import org.springframework.http.codec.support.DefaultClientCodecConfigurer;
import static org.assertj.core.api.Assertions.assertThat;
@@ -132,6 +139,40 @@ class CodecsAutoConfigurationTests {
1048576));
}
@Test
void kotlinSerializationUsesLimitedPredicateWhenOtherJsonConverterIsAvailable() {
this.contextRunner.withUserConfiguration(KotlinxJsonConfiguration.class).run((context) -> {
KotlinSerializationJsonEncoder encoder = findEncoder(context, KotlinSerializationJsonEncoder.class);
assertThat(encoder.canEncode(ResolvableType.forClass(Map.class), MediaType.APPLICATION_JSON)).isFalse();
});
}
@Test
void kotlinSerializationUsesUnrestrictedPredicateWhenNoOtherJsonConverterIsAvailable() {
FilteredClassLoader classLoader = new FilteredClassLoader(JsonMapper.class.getPackage().getName(),
ObjectMapper.class.getPackage().getName());
this.contextRunner.withClassLoader(classLoader)
.withUserConfiguration(KotlinxJsonConfiguration.class)
.run((context) -> {
KotlinSerializationJsonEncoder encoder = findEncoder(context, KotlinSerializationJsonEncoder.class);
assertThat(encoder.canEncode(ResolvableType.forClass(Map.class), MediaType.APPLICATION_JSON)).isTrue();
});
}
@SuppressWarnings("unchecked")
private <T> T findEncoder(AssertableApplicationContext context, Class<T> encoderClass) {
ServerCodecConfigurer configurer = ServerCodecConfigurer.create();
context.getBeansOfType(CodecCustomizer.class).values().forEach((codec) -> codec.customize(configurer));
return (T) configurer.getWriters()
.stream()
.filter((writer) -> writer instanceof EncoderHttpMessageWriter<?>)
.map((writer) -> (EncoderHttpMessageWriter<?>) writer)
.map(EncoderHttpMessageWriter::getEncoder)
.filter((encoder) -> encoderClass.isAssignableFrom(encoder.getClass()))
.findFirst()
.orElseThrow();
}
private DefaultCodecs defaultCodecs(AssertableApplicationContext context) {
CodecCustomizer customizer = context.getBean(CodecCustomizer.class);
CodecConfigurer configurer = new DefaultClientCodecConfigurer();
@@ -159,6 +200,16 @@ class CodecsAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class KotlinxJsonConfiguration {
@Bean
Json kotlinxJson() {
return Json.Default;
}
}
@Configuration(proxyBeanMethods = false)
static class CodecCustomizerConfiguration {