Configure custom Kotlin Serialization converter

Prior to this commit, `HttpMessageConverters` would consider the Kotlin
Serialization JSON converter as an alternative to the Jackson variant.
As seen in related issues, this converter is more commonly used for
annotated classes specifically and applications often rely on Jackson
acting as a fallback for types not supported by Kotlin Serialization.

This commit enables applications to configure such a converter on
`HttpMessageConverters` and order it ahead of of the JSON one.

Closes gh-35733
This commit is contained in:
Brian Clozel
2025-11-03 14:33:12 +01:00
parent 1fc446b3fc
commit e6a076017b
6 changed files with 184 additions and 176 deletions
@@ -31,6 +31,7 @@ import org.springframework.http.converter.cbor.JacksonCborHttpMessageConverter;
import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter;
import org.springframework.http.converter.feed.RssChannelHttpMessageConverter;
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
import org.springframework.http.converter.protobuf.KotlinSerializationProtobufHttpMessageConverter;
import org.springframework.http.converter.smile.JacksonSmileHttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
@@ -63,42 +64,42 @@ class DefaultHttpMessageConvertersTests {
void failsWhenStringConverterDoesNotSupportMediaType() {
assertThatIllegalArgumentException()
.isThrownBy(() -> HttpMessageConverters.forClient().withStringConverter(new CustomHttpMessageConverter()).build())
.withMessage("stringMessageConverter should support 'text/plain'");
.withMessage("stringConverter should support 'text/plain'");
}
@Test
void failsWhenJsonConverterDoesNotSupportMediaType() {
assertThatIllegalArgumentException()
.isThrownBy(() -> HttpMessageConverters.forClient().withJsonConverter(new CustomHttpMessageConverter()).build())
.withMessage("jsonMessageConverter should support 'application/json'");
.withMessage("jsonConverter should support 'application/json'");
}
@Test
void failsWhenXmlConverterDoesNotSupportMediaType() {
assertThatIllegalArgumentException()
.isThrownBy(() -> HttpMessageConverters.forClient().withXmlConverter(new CustomHttpMessageConverter()).build())
.withMessage("xmlMessageConverter should support 'text/xml'");
.withMessage("xmlConverter should support 'text/xml'");
}
@Test
void failsWhenSmileConverterDoesNotSupportMediaType() {
assertThatIllegalArgumentException()
.isThrownBy(() -> HttpMessageConverters.forClient().withSmileConverter(new CustomHttpMessageConverter()).build())
.withMessage("smileMessageConverter should support 'application/x-jackson-smile'");
.withMessage("smileConverter should support 'application/x-jackson-smile'");
}
@Test
void failsWhenCborConverterDoesNotSupportMediaType() {
assertThatIllegalArgumentException()
.isThrownBy(() -> HttpMessageConverters.forClient().withCborConverter(new CustomHttpMessageConverter()).build())
.withMessage("cborMessageConverter should support 'application/cbor'");
.withMessage("cborConverter should support 'application/cbor'");
}
@Test
void failsWhenYamlConverterDoesNotSupportMediaType() {
assertThatIllegalArgumentException()
.isThrownBy(() -> HttpMessageConverters.forClient().withYamlConverter(new CustomHttpMessageConverter()).build())
.withMessage("yamlMessageConverter should support 'application/yaml'");
.withMessage("yamlConverter should support 'application/yaml'");
}
@@ -110,7 +111,7 @@ class DefaultHttpMessageConvertersTests {
var converters = HttpMessageConverters.forClient().registerDefaults().build();
assertThat(converters).hasExactlyElementsOfTypes(ByteArrayHttpMessageConverter.class,
StringHttpMessageConverter.class, ResourceHttpMessageConverter.class,
AllEncompassingFormHttpMessageConverter.class,
AllEncompassingFormHttpMessageConverter.class, KotlinSerializationJsonHttpMessageConverter.class,
JacksonJsonHttpMessageConverter.class, JacksonSmileHttpMessageConverter.class,
JacksonCborHttpMessageConverter.class, JacksonYamlHttpMessageConverter.class,
JacksonXmlHttpMessageConverter.class, KotlinSerializationProtobufHttpMessageConverter.class,
@@ -124,11 +125,11 @@ class DefaultHttpMessageConvertersTests {
assertThat(multipartConverter.getPartConverters()).hasExactlyElementsOfTypes(
ByteArrayHttpMessageConverter.class, StringHttpMessageConverter.class,
ResourceHttpMessageConverter.class, JacksonJsonHttpMessageConverter.class,
JacksonSmileHttpMessageConverter.class, JacksonCborHttpMessageConverter.class,
JacksonYamlHttpMessageConverter.class, JacksonXmlHttpMessageConverter.class,
KotlinSerializationProtobufHttpMessageConverter.class, AtomFeedHttpMessageConverter.class,
RssChannelHttpMessageConverter.class);
ResourceHttpMessageConverter.class, KotlinSerializationJsonHttpMessageConverter.class,
JacksonJsonHttpMessageConverter.class, JacksonSmileHttpMessageConverter.class,
JacksonCborHttpMessageConverter.class, JacksonYamlHttpMessageConverter.class,
JacksonXmlHttpMessageConverter.class, KotlinSerializationProtobufHttpMessageConverter.class,
AtomFeedHttpMessageConverter.class, RssChannelHttpMessageConverter.class);
}
@Test
@@ -145,7 +146,7 @@ class DefaultHttpMessageConvertersTests {
assertThat(converters).hasExactlyElementsOfTypes(
CustomHttpMessageConverter.class, ByteArrayHttpMessageConverter.class,
StringHttpMessageConverter.class, ResourceHttpMessageConverter.class,
AllEncompassingFormHttpMessageConverter.class,
AllEncompassingFormHttpMessageConverter.class, KotlinSerializationJsonHttpMessageConverter.class,
JacksonJsonHttpMessageConverter.class, JacksonSmileHttpMessageConverter.class,
JacksonCborHttpMessageConverter.class, JacksonYamlHttpMessageConverter.class,
JacksonXmlHttpMessageConverter.class, KotlinSerializationProtobufHttpMessageConverter.class,
@@ -213,7 +214,7 @@ class DefaultHttpMessageConvertersTests {
assertThat(converters).hasExactlyElementsOfTypes(
ByteArrayHttpMessageConverter.class, StringHttpMessageConverter.class,
ResourceHttpMessageConverter.class, ResourceRegionHttpMessageConverter.class,
AllEncompassingFormHttpMessageConverter.class,
AllEncompassingFormHttpMessageConverter.class, KotlinSerializationJsonHttpMessageConverter.class,
JacksonJsonHttpMessageConverter.class, JacksonSmileHttpMessageConverter.class,
JacksonCborHttpMessageConverter.class, JacksonYamlHttpMessageConverter.class,
JacksonXmlHttpMessageConverter.class, KotlinSerializationProtobufHttpMessageConverter.class,
@@ -227,11 +228,11 @@ class DefaultHttpMessageConvertersTests {
assertThat(multipartConverter.getPartConverters()).hasExactlyElementsOfTypes(
ByteArrayHttpMessageConverter.class, StringHttpMessageConverter.class,
ResourceHttpMessageConverter.class, JacksonJsonHttpMessageConverter.class,
JacksonSmileHttpMessageConverter.class, JacksonCborHttpMessageConverter.class,
JacksonYamlHttpMessageConverter.class, JacksonXmlHttpMessageConverter.class,
KotlinSerializationProtobufHttpMessageConverter.class, AtomFeedHttpMessageConverter.class,
RssChannelHttpMessageConverter.class);
ResourceHttpMessageConverter.class, KotlinSerializationJsonHttpMessageConverter.class,
JacksonJsonHttpMessageConverter.class, JacksonSmileHttpMessageConverter.class,
JacksonCborHttpMessageConverter.class, JacksonYamlHttpMessageConverter.class,
JacksonXmlHttpMessageConverter.class, KotlinSerializationProtobufHttpMessageConverter.class,
AtomFeedHttpMessageConverter.class, RssChannelHttpMessageConverter.class);
}
@Test
@@ -249,7 +250,7 @@ class DefaultHttpMessageConvertersTests {
CustomHttpMessageConverter.class,
ByteArrayHttpMessageConverter.class, StringHttpMessageConverter.class,
ResourceHttpMessageConverter.class, ResourceRegionHttpMessageConverter.class,
AllEncompassingFormHttpMessageConverter.class,
AllEncompassingFormHttpMessageConverter.class, KotlinSerializationJsonHttpMessageConverter.class,
JacksonJsonHttpMessageConverter.class, JacksonSmileHttpMessageConverter.class,
JacksonCborHttpMessageConverter.class, JacksonYamlHttpMessageConverter.class,
JacksonXmlHttpMessageConverter.class, KotlinSerializationProtobufHttpMessageConverter.class,
@@ -605,6 +605,12 @@ class JacksonJsonHttpMessageConverterTests {
assertThat(result).contains("\"string\":\"Foo\"");
}
@Test
void readEmptyAsNull() throws IOException {
MockHttpInputMessage inputMessage = new MockHttpInputMessage("null".getBytes(StandardCharsets.UTF_8));
inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
assertThat(this.converter.read(Map.class, inputMessage)).isNull();
}
interface MyInterface {
@@ -25,7 +25,6 @@ import java.lang.annotation.Target;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
@@ -339,24 +338,6 @@ class RestClientIntegrationTests {
});
}
@ParameterizedRestClientTest
@SuppressWarnings({ "rawtypes", "unchecked" })
void retrieveJsonNull(ClientHttpRequestFactory requestFactory) throws IOException {
startServer(requestFactory);
prepareResponse(builder -> builder
.code(200)
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.body("null"));
Map result = this.restClient.get()
.uri("/null")
.retrieve()
.body(Map.class);
assertThat(result).isNull();
}
@ParameterizedRestClientTest
void retrieveJsonEmpty(ClientHttpRequestFactory requestFactory) throws IOException {
startServer(requestFactory);
@@ -53,8 +53,6 @@ import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.SmartHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.util.DefaultUriBuilderFactory;
@@ -108,18 +106,6 @@ class RestTemplateTests {
template.setErrorHandler(errorHandler);
}
@Test
void defaultMessageConvertersWithoutKotlinSerialization() {
RestTemplate restTemplate = new RestTemplate();
List<HttpMessageConverter<?>> httpMessageConverters = restTemplate.getMessageConverters();
assertThat(httpMessageConverters).extracting("class").containsOnlyOnce(
JacksonJsonHttpMessageConverter.class
);
assertThat(httpMessageConverters).extracting("class").doesNotContain(
KotlinSerializationJsonHttpMessageConverter.class
);
}
@Test
void constructorPreconditions() {
assertThatIllegalArgumentException()