Restrict Kotlin serialization when alternative is available

Previously, Kotlin Serialization would be used too aggressively then
an alternative JSON converter was available. This could lead to
unwanted results when a response should have been serialized using
Jackson, for example, rather than Kotlin Serialization.

This commit addresses this by only allowing Kotlin Serialization to
serialize types that are not annotated with @Serializable when no
alternative JSON converter is available.

Fixes gh-48070
This commit is contained in:
Andy Wilkinson
2025-11-12 10:00:45 +00:00
parent 2e52c3c35e
commit 58cdf71b52
2 changed files with 39 additions and 2 deletions
@@ -19,11 +19,14 @@ package org.springframework.boot.http.converter.autoconfigure;
import kotlinx.serialization.Serializable;
import kotlinx.serialization.json.Json;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
/**
@@ -39,8 +42,23 @@ class KotlinSerializationHttpMessageConvertersConfiguration {
@Bean
@ConditionalOnMissingBean
KotlinSerializationJsonHttpMessageConverter kotlinSerializationJsonHttpMessageConverter(Json json) {
return new KotlinSerializationJsonHttpMessageConverter(json);
KotlinSerializationJsonHttpMessageConverter kotlinSerializationJsonHttpMessageConverter(Json json,
ObjectProvider<HttpMessageConverter<?>> converters) {
return supportsApplicationJson(converters) ? new KotlinSerializationJsonHttpMessageConverter(json)
: new KotlinSerializationJsonHttpMessageConverter(json, (type) -> true);
}
private boolean supportsApplicationJson(ObjectProvider<HttpMessageConverter<?>> converters) {
return converters.orderedStream().filter(this::supportsApplicationJson).findFirst().isPresent();
}
private boolean supportsApplicationJson(HttpMessageConverter<?> converter) {
for (MediaType mediaType : converter.getSupportedMediaTypes()) {
if (!mediaType.equals(MediaType.ALL) && mediaType.isCompatibleWith(MediaType.APPLICATION_JSON)) {
return true;
}
}
return false;
}
}
@@ -47,6 +47,7 @@ import org.springframework.context.support.GenericApplicationContext;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.server.mvc.TypeConstrainedJacksonJsonHttpMessageConverter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverters;
import org.springframework.http.converter.HttpMessageConverters.ClientBuilder;
@@ -229,6 +230,24 @@ class HttpMessageConvertersAutoConfigurationTests {
});
}
@Test
void kotlinSerializationUsesLimitedPredicateWhenOtherJsonConverterIsAvailable() {
allOptionsRunner().run((context) -> {
KotlinSerializationJsonHttpMessageConverter converter = context
.getBean(KotlinSerializationJsonHttpMessageConverter.class);
assertThat(converter.canWrite(Map.class, MediaType.APPLICATION_JSON)).isFalse();
});
}
@Test
void kotlinSerializationUsesUnrestrictedPredicateWhenNoOtherJsonConverterIsAvailable() {
this.contextRunner.withBean(Json.class, () -> Json.Default).run((context) -> {
KotlinSerializationJsonHttpMessageConverter converter = context
.getBean(KotlinSerializationJsonHttpMessageConverter.class);
assertThat(converter.canWrite(Map.class, MediaType.APPLICATION_JSON)).isTrue();
});
}
@Test
void stringDefaultConverter() {
this.contextRunner.run(assertConverter(StringHttpMessageConverter.class, "stringHttpMessageConverter"));