Skip to content

Commit d161aaf

Browse files
committed
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
1 parent eb381d6 commit d161aaf

File tree

3 files changed

+80
-0
lines changed

3 files changed

+80
-0
lines changed

module/spring-boot-http-codec/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ dependencies {
3232
optional(project(":core:spring-boot-test"))
3333
optional(project(":module:spring-boot-jackson"))
3434
optional(project(":module:spring-boot-jackson2"))
35+
optional(project(":module:spring-boot-kotlinx-serialization-json"))
3536
optional("org.springframework:spring-webflux")
3637

3738
testImplementation(project(":core:spring-boot-test"))

module/spring-boot-http-codec/src/main/java/org/springframework/boot/http/codec/autoconfigure/CodecsAutoConfiguration.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.boot.http.codec.autoconfigure;
1818

1919
import com.fasterxml.jackson.databind.ObjectMapper;
20+
import kotlinx.serialization.json.Json;
2021
import org.jspecify.annotations.Nullable;
2122
import tools.jackson.databind.json.JsonMapper;
2223

@@ -35,9 +36,13 @@
3536
import org.springframework.context.annotation.Configuration;
3637
import org.springframework.core.Ordered;
3738
import org.springframework.core.annotation.Order;
39+
import org.springframework.core.io.ResourceLoader;
3840
import org.springframework.http.codec.CodecConfigurer;
3941
import org.springframework.http.codec.json.JacksonJsonDecoder;
4042
import org.springframework.http.codec.json.JacksonJsonEncoder;
43+
import org.springframework.http.codec.json.KotlinSerializationJsonDecoder;
44+
import org.springframework.http.codec.json.KotlinSerializationJsonEncoder;
45+
import org.springframework.util.ClassUtils;
4146
import org.springframework.util.unit.DataSize;
4247
import org.springframework.web.reactive.function.client.WebClient;
4348

@@ -93,6 +98,29 @@ CodecCustomizer jackson2CodecCustomizer(ObjectMapper objectMapper) {
9398

9499
}
95100

101+
@Configuration(proxyBeanMethods = false)
102+
@ConditionalOnClass(Json.class)
103+
static class KotlinxSerializationJsonCodecConfiguration {
104+
105+
@Bean
106+
@ConditionalOnBean(Json.class)
107+
CodecCustomizer kotlinxJsonCodecCustomizer(Json json, ResourceLoader resourceLoader) {
108+
ClassLoader classLoader = resourceLoader.getClassLoader();
109+
boolean hasAnyJsonSupport = ClassUtils.isPresent("tools.jackson.databind.json.JsonMapper", classLoader)
110+
|| ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
111+
|| ClassUtils.isPresent("com.google.gson.Gson", classLoader);
112+
113+
return (configurer) -> {
114+
CodecConfigurer.DefaultCodecs defaults = configurer.defaultCodecs();
115+
defaults.kotlinSerializationJsonEncoder(hasAnyJsonSupport ? new KotlinSerializationJsonEncoder(json)
116+
: new KotlinSerializationJsonEncoder(json, (type) -> true));
117+
defaults.kotlinSerializationJsonDecoder(hasAnyJsonSupport ? new KotlinSerializationJsonDecoder(json)
118+
: new KotlinSerializationJsonDecoder(json, (type) -> true));
119+
};
120+
}
121+
122+
}
123+
96124
@Configuration(proxyBeanMethods = false)
97125
@EnableConfigurationProperties(HttpCodecsProperties.class)
98126
static class DefaultCodecsConfiguration {

module/spring-boot-http-codec/src/test/java/org/springframework/boot/http/codec/autoconfigure/CodecsAutoConfigurationTests.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
package org.springframework.boot.http.codec.autoconfigure;
1818

1919
import java.util.List;
20+
import java.util.Map;
2021

2122
import com.fasterxml.jackson.databind.ObjectMapper;
23+
import kotlinx.serialization.json.Json;
2224
import org.junit.jupiter.api.Test;
2325
import tools.jackson.databind.json.JsonMapper;
2426

@@ -30,8 +32,13 @@
3032
import org.springframework.context.annotation.Bean;
3133
import org.springframework.context.annotation.Configuration;
3234
import org.springframework.core.Ordered;
35+
import org.springframework.core.ResolvableType;
36+
import org.springframework.http.MediaType;
3337
import org.springframework.http.codec.CodecConfigurer;
3438
import org.springframework.http.codec.CodecConfigurer.DefaultCodecs;
39+
import org.springframework.http.codec.EncoderHttpMessageWriter;
40+
import org.springframework.http.codec.ServerCodecConfigurer;
41+
import org.springframework.http.codec.json.KotlinSerializationJsonEncoder;
3542
import org.springframework.http.codec.support.DefaultClientCodecConfigurer;
3643

3744
import static org.assertj.core.api.Assertions.assertThat;
@@ -132,6 +139,40 @@ void maxInMemorySizeEnforcedInDefaultCodecs() {
132139
1048576));
133140
}
134141

142+
@Test
143+
void kotlinSerializationUsesLimitedPredicateWhenOtherJsonConverterIsAvailable() {
144+
this.contextRunner.withUserConfiguration(KotlinxJsonConfiguration.class).run((context) -> {
145+
KotlinSerializationJsonEncoder encoder = findEncoder(context, KotlinSerializationJsonEncoder.class);
146+
assertThat(encoder.canEncode(ResolvableType.forClass(Map.class), MediaType.APPLICATION_JSON)).isFalse();
147+
});
148+
}
149+
150+
@Test
151+
void kotlinSerializationUsesUnrestrictedPredicateWhenNoOtherJsonConverterIsAvailable() {
152+
FilteredClassLoader classLoader = new FilteredClassLoader(JsonMapper.class.getPackage().getName(),
153+
ObjectMapper.class.getPackage().getName());
154+
this.contextRunner.withClassLoader(classLoader)
155+
.withUserConfiguration(KotlinxJsonConfiguration.class)
156+
.run((context) -> {
157+
KotlinSerializationJsonEncoder encoder = findEncoder(context, KotlinSerializationJsonEncoder.class);
158+
assertThat(encoder.canEncode(ResolvableType.forClass(Map.class), MediaType.APPLICATION_JSON)).isTrue();
159+
});
160+
}
161+
162+
@SuppressWarnings("unchecked")
163+
private <T> T findEncoder(AssertableApplicationContext context, Class<T> encoderClass) {
164+
ServerCodecConfigurer configurer = ServerCodecConfigurer.create();
165+
context.getBeansOfType(CodecCustomizer.class).values().forEach((codec) -> codec.customize(configurer));
166+
return (T) configurer.getWriters()
167+
.stream()
168+
.filter((writer) -> writer instanceof EncoderHttpMessageWriter<?>)
169+
.map((writer) -> (EncoderHttpMessageWriter<?>) writer)
170+
.map(EncoderHttpMessageWriter::getEncoder)
171+
.filter((encoder) -> encoderClass.isAssignableFrom(encoder.getClass()))
172+
.findFirst()
173+
.orElseThrow();
174+
}
175+
135176
private DefaultCodecs defaultCodecs(AssertableApplicationContext context) {
136177
CodecCustomizer customizer = context.getBean(CodecCustomizer.class);
137178
CodecConfigurer configurer = new DefaultClientCodecConfigurer();
@@ -159,6 +200,16 @@ ObjectMapper objectMapper() {
159200

160201
}
161202

203+
@Configuration(proxyBeanMethods = false)
204+
static class KotlinxJsonConfiguration {
205+
206+
@Bean
207+
Json kotlinxJson() {
208+
return Json.Default;
209+
}
210+
211+
}
212+
162213
@Configuration(proxyBeanMethods = false)
163214
static class CodecCustomizerConfiguration {
164215

0 commit comments

Comments
 (0)