Add kotlinx.serialization JSON support to Spring WebFlux

Flow decoding is not supported yet since it depends on
kotlin/kotlinx.serialization#1073, but it will be
enabled when this issue will be fixed.

Closes gh-25771
This commit is contained in:
Sébastien Deleuze
2020-10-06 23:02:14 +02:00
parent 1cd8871d7f
commit 92b2c45281
9 changed files with 479 additions and 6 deletions

View File

@@ -176,6 +176,22 @@ public interface CodecConfigurer {
*/
void jaxb2Encoder(Encoder<?> encoder);
/**
* Override the default Kotlin Serialization JSON {@code Decoder}.
* @param decoder the decoder instance to use
* @since 5.3
* @see org.springframework.http.codec.json.KotlinSerializationJsonDecoder
*/
void kotlinSerializationJsonDecoder(Decoder<?> decoder);
/**
* Override the default Kotlin Serialization JSON {@code Encoder}.
* @param encoder the encoder instance to use
* @since 5.3
* @see org.springframework.http.codec.json.KotlinSerializationJsonEncoder
*/
void kotlinSerializationJsonEncoder(Encoder<?> encoder);
/**
* Configure a limit on the number of bytes that can be buffered whenever
* the input stream needs to be aggregated. This can be a result of

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.codec.json;
import java.lang.reflect.Type;
import java.util.Map;
import kotlinx.serialization.KSerializer;
import kotlinx.serialization.SerializersKt;
import kotlinx.serialization.json.Json;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.AbstractDecoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.MediaType;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.MimeType;
/**
* Decode a byte stream into JSON and convert to Object's with
* <a href="https://github.com/Kotlin/kotlinx.serialization">kotlinx.serialization</a>.
*
* <p>This decoder can be used to bind {@code @Serializable} Kotlin classes.
* It supports {@code application/json} and {@code application/*+json} with
* various character sets, {@code UTF-8} being the default.
*
* <p>Decoding streams is not supported yet, see
* <a href="https://github.com/Kotlin/kotlinx.serialization/issues/1073">kotlinx.serialization/issues/1073</a>
* related issue.
*
* @author Sebastien Deleuze
* @since 5.3
*/
public class KotlinSerializationJsonDecoder extends AbstractDecoder<Object> {
private static final Map<Type, KSerializer<Object>> serializerCache = new ConcurrentReferenceHashMap<>();
private final Json json;
// String decoding needed for now, see https://github.com/Kotlin/kotlinx.serialization/issues/204 for more details
private final StringDecoder stringDecoder = StringDecoder.allMimeTypes(StringDecoder.DEFAULT_DELIMITERS, false);
public KotlinSerializationJsonDecoder() {
this(Json.Default);
}
public KotlinSerializationJsonDecoder(Json json) {
super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
this.json = json;
}
@Override
public boolean canDecode(ResolvableType elementType, MimeType mimeType) {
return super.canDecode(elementType, mimeType) && (!CharSequence.class.isAssignableFrom(elementType.toClass()));
}
@Override
public Flux<Object> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType, MimeType mimeType, Map<String, Object> hints) {
return Flux.error(new UnsupportedOperationException());
}
@Override
public Mono<Object> decodeToMono(Publisher<DataBuffer> inputStream, ResolvableType elementType, MimeType mimeType, Map<String, Object> hints) {
return stringDecoder
.decodeToMono(inputStream, elementType, mimeType, hints)
.map(jsonText -> this.json.decodeFromString(serializer(elementType.getType()), jsonText));
}
/**
* Tries to find a serializer that can marshall or unmarshall instances of the given type
* using kotlinx.serialization. If no serializer can be found, an exception is thrown.
* <p>Resolved serializers are cached and cached results are returned on successive calls.
* @param type the type to find a serializer for
* @return a resolved serializer for the given type
* @throws RuntimeException if no serializer supporting the given type can be found
*/
private KSerializer<Object> serializer(Type type) {
KSerializer<Object> serializer = serializerCache.get(type);
if (serializer == null) {
serializer = SerializersKt.serializer(type);
serializerCache.put(type, serializer);
}
return serializer;
}
}

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.codec.json;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import kotlinx.serialization.KSerializer;
import kotlinx.serialization.SerializersKt;
import kotlinx.serialization.json.Json;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.AbstractEncoder;
import org.springframework.core.codec.CharSequenceEncoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.MimeType;
/**
* Encode from an {@code Object} stream to a byte stream of JSON objects using
* <a href="https://github.com/Kotlin/kotlinx.serialization">kotlinx.serialization</a>.
*
* <p>This encoder can be used to bind {@code @Serializable} Kotlin classes.
* It supports {@code application/json} and {@code application/*+json} with
* various character sets, {@code UTF-8} being the default.
*
* @author Sebastien Deleuze
* @since 5.3
*/
public class KotlinSerializationJsonEncoder extends AbstractEncoder<Object> {
private static final Map<Type, KSerializer<Object>> serializerCache = new ConcurrentReferenceHashMap<>();
private final Json json;
// CharSequence encoding needed for now, see https://github.com/Kotlin/kotlinx.serialization/issues/204 for more details
private final CharSequenceEncoder charSequenceEncoder = CharSequenceEncoder.allMimeTypes();
public KotlinSerializationJsonEncoder() {
this(Json.Default);
}
public KotlinSerializationJsonEncoder(Json json) {
super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
this.json = json;
}
@Override
public boolean canEncode(ResolvableType elementType, MimeType mimeType) {
return super.canEncode(elementType, mimeType)
&& (!String.class.isAssignableFrom(elementType.toClass()))
&& (!ServerSentEvent.class.isAssignableFrom(elementType.toClass()));
}
@Override
public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory, ResolvableType elementType, MimeType mimeType, Map<String, Object> hints) {
if (inputStream instanceof Mono) {
return Mono.from(inputStream)
.map(value -> encodeValue(value, bufferFactory, elementType, mimeType, hints))
.flux();
}
else {
ResolvableType listType = ResolvableType.forClassWithGenerics(List.class, elementType);
return Flux.from(inputStream)
.collectList()
.map(list -> encodeValue(list, bufferFactory, listType, mimeType, hints))
.flux();
}
}
@Override
public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, ResolvableType valueType, MimeType mimeType, Map<String, Object> hints) {
String json = this.json.encodeToString(serializer(valueType.getType()), value);
return charSequenceEncoder.encodeValue(json, bufferFactory, valueType, mimeType, null);
}
/**
* Tries to find a serializer that can marshall or unmarshall instances of the given type
* using kotlinx.serialization. If no serializer can be found, an exception is thrown.
* <p>Resolved serializers are cached and cached results are returned on successive calls.
* @param type the type to find a serializer for
* @return a resolved serializer for the given type
* @throws RuntimeException if no serializer supporting the given type can be found
*/
private KSerializer<Object> serializer(Type type) {
KSerializer<Object> serializer = serializerCache.get(type);
if (serializer == null) {
serializer = SerializersKt.serializer(type);
serializerCache.put(type, serializer);
}
return serializer;
}
}

View File

@@ -51,6 +51,8 @@ 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;
@@ -91,6 +93,8 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
static final boolean nettyByteBufPresent;
static final boolean kotlinSerializationJsonPresent;
static {
ClassLoader classLoader = BaseCodecConfigurer.class.getClassLoader();
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
@@ -100,6 +104,7 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
protobufPresent = ClassUtils.isPresent("com.google.protobuf.Message", classLoader);
synchronossMultipartPresent = ClassUtils.isPresent("org.synchronoss.cloud.nio.multipart.NioMultipartParser", classLoader);
nettyByteBufPresent = ClassUtils.isPresent("io.netty.buffer.ByteBuf", classLoader);
kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader);
}
@@ -127,6 +132,12 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
@Nullable
private Encoder<?> jaxb2Encoder;
@Nullable
private Decoder<?> kotlinSerializationJsonDecoder;
@Nullable
private Encoder<?> kotlinSerializationJsonEncoder;
@Nullable
private Integer maxInMemorySize;
@@ -151,6 +162,8 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
this.protobufEncoder = other.protobufEncoder;
this.jaxb2Decoder = other.jaxb2Decoder;
this.jaxb2Encoder = other.jaxb2Encoder;
this.kotlinSerializationJsonDecoder = other.kotlinSerializationJsonDecoder;
this.kotlinSerializationJsonEncoder = other.kotlinSerializationJsonEncoder;
this.maxInMemorySize = other.maxInMemorySize;
this.enableLoggingRequestDetails = other.enableLoggingRequestDetails;
this.registerDefaults = other.registerDefaults;
@@ -196,6 +209,16 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
this.jaxb2Encoder = encoder;
}
@Override
public void kotlinSerializationJsonDecoder(Decoder<?> decoder) {
this.kotlinSerializationJsonDecoder = decoder;
}
@Override
public void kotlinSerializationJsonEncoder(Encoder<?> encoder) {
this.kotlinSerializationJsonEncoder = encoder;
}
@Override
public void maxInMemorySize(int byteCount) {
this.maxInMemorySize = byteCount;
@@ -365,6 +388,9 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
if (jackson2Present) {
addCodec(readers, new DecoderHttpMessageReader<>(getJackson2JsonDecoder()));
}
else if (kotlinSerializationJsonPresent) {
addCodec(readers, new DecoderHttpMessageReader<>(getKotlinSerializationJsonDecoder()));
}
if (jackson2SmilePresent) {
addCodec(readers, new DecoderHttpMessageReader<>(this.jackson2SmileDecoder != null ?
(Jackson2SmileDecoder) this.jackson2SmileDecoder : new Jackson2SmileDecoder()));
@@ -461,6 +487,9 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
if (jackson2Present) {
writers.add(new EncoderHttpMessageWriter<>(getJackson2JsonEncoder()));
}
else if (kotlinSerializationJsonPresent) {
writers.add(new EncoderHttpMessageWriter<>(getKotlinSerializationJsonEncoder()));
}
if (jackson2SmilePresent) {
writers.add(new EncoderHttpMessageWriter<>(this.jackson2SmileEncoder != null ?
(Jackson2SmileEncoder) this.jackson2SmileEncoder : new Jackson2SmileEncoder()));
@@ -522,4 +551,18 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
return this.jackson2JsonEncoder;
}
protected Decoder<?> getKotlinSerializationJsonDecoder() {
if (this.kotlinSerializationJsonDecoder == null) {
this.kotlinSerializationJsonDecoder = new KotlinSerializationJsonDecoder();
}
return this.kotlinSerializationJsonDecoder;
}
protected Encoder<?> getKotlinSerializationJsonEncoder() {
if (this.kotlinSerializationJsonEncoder == null) {
this.kotlinSerializationJsonEncoder = new KotlinSerializationJsonEncoder();
}
return this.kotlinSerializationJsonEncoder;
}
}

View File

@@ -96,9 +96,10 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo
@Override
protected void extendObjectReaders(List<HttpMessageReader<?>> objectReaders) {
Decoder<?> decoder = (this.sseDecoder != null ?
this.sseDecoder :
jackson2Present ? getJackson2JsonDecoder() : null);
Decoder<?> decoder = (this.sseDecoder != null ? this.sseDecoder :
jackson2Present ? getJackson2JsonDecoder() :
kotlinSerializationJsonPresent ? getKotlinSerializationJsonDecoder() :
null);
addCodec(objectReaders, new ServerSentEventHttpMessageReader(decoder));
}

View File

@@ -86,7 +86,10 @@ class ServerDefaultCodecsImpl extends BaseDefaultCodecs implements ServerCodecCo
@Nullable
private Encoder<?> getSseEncoder() {
return this.sseEncoder != null ? this.sseEncoder : jackson2Present ? getJackson2JsonEncoder() : null;
return this.sseEncoder != null ? this.sseEncoder :
jackson2Present ? getJackson2JsonEncoder() :
kotlinSerializationJsonPresent ? getKotlinSerializationJsonEncoder() :
null;
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.codec.json
import kotlinx.serialization.Serializable
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.springframework.core.ResolvableType
import org.springframework.core.io.buffer.DataBuffer
import org.springframework.core.testfixture.codec.AbstractDecoderTests
import org.springframework.http.MediaType
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.test.StepVerifier
import reactor.test.StepVerifier.FirstStep
import java.lang.UnsupportedOperationException
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
/**
* Tests for the JSON decoding using kotlinx.serialization.
*
* @author Sebastien Deleuze
*/
class KotlinSerializationJsonDecoderTests : AbstractDecoderTests<KotlinSerializationJsonDecoder>(KotlinSerializationJsonDecoder()) {
@Suppress("UsePropertyAccessSyntax", "DEPRECATION")
@Test
override fun canDecode() {
val jsonSubtype = MediaType("application", "vnd.test-micro-type+json")
Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java), MediaType.APPLICATION_JSON)).isTrue()
Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java), jsonSubtype)).isTrue()
Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java), null)).isTrue()
Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(String::class.java), null)).isFalse()
Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java), MediaType.APPLICATION_XML)).isFalse()
Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java),
MediaType("application", "json", StandardCharsets.UTF_8))).isTrue()
Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java),
MediaType("application", "json", StandardCharsets.US_ASCII))).isTrue()
Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java),
MediaType("application", "json", StandardCharsets.ISO_8859_1))).isTrue()
}
@Test
override fun decode() {
val output = decoder.decode(Mono.empty(), ResolvableType.forClass(Pojo::class.java), null, emptyMap())
StepVerifier
.create(output)
.expectError(UnsupportedOperationException::class.java)
}
@Test
override fun decodeToMono() {
val input = Flux.concat(
stringBuffer("[{\"bar\":\"b1\",\"foo\":\"f1\"},"),
stringBuffer("{\"bar\":\"b2\",\"foo\":\"f2\"}]"))
val elementType = ResolvableType.forClassWithGenerics(List::class.java, Pojo::class.java)
testDecodeToMonoAll(input, elementType, { step: FirstStep<Any> ->
step
.expectNext(listOf(Pojo("f1", "b1"), Pojo("f2", "b2")))
.expectComplete()
.verify()
}, null, null)
}
private fun stringBuffer(value: String): Mono<DataBuffer> {
return stringBuffer(value, StandardCharsets.UTF_8)
}
private fun stringBuffer(value: String, charset: Charset): Mono<DataBuffer> {
return Mono.defer {
val bytes = value.toByteArray(charset)
val buffer = bufferFactory.allocateBuffer(bytes.size)
buffer.write(bytes)
Mono.just(buffer)
}
}
@Serializable
data class Pojo(val foo: String, val bar: String)
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.codec.json
import kotlinx.serialization.Serializable
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.springframework.core.ResolvableType
import org.springframework.core.io.buffer.DataBuffer
import org.springframework.core.io.buffer.DataBufferUtils
import org.springframework.core.testfixture.codec.AbstractEncoderTests
import org.springframework.http.MediaType
import org.springframework.http.codec.ServerSentEvent
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.test.StepVerifier.FirstStep
import java.nio.charset.StandardCharsets
/**
* Tests for the JSON encoding using kotlinx.serialization.
*
* @author Sebastien Deleuze
*/
@Suppress("UsePropertyAccessSyntax")
class KotlinSerializationJsonEncoderTests : AbstractEncoderTests<KotlinSerializationJsonEncoder>(KotlinSerializationJsonEncoder()) {
@Test
override fun canEncode() {
val pojoType = ResolvableType.forClass(Pojo::class.java)
val jsonSubtype = MediaType("application", "vnd.test-micro-type+json")
Assertions.assertThat(encoder.canEncode(pojoType, MediaType.APPLICATION_JSON)).isTrue()
Assertions.assertThat(encoder.canEncode(pojoType, jsonSubtype)).isTrue()
Assertions.assertThat(encoder.canEncode(pojoType, null)).isTrue()
Assertions.assertThat(encoder.canEncode(ResolvableType.forClass(Pojo::class.java),
MediaType("application", "json", StandardCharsets.UTF_8))).isTrue()
Assertions.assertThat(encoder.canEncode(ResolvableType.forClass(Pojo::class.java),
MediaType("application", "json", StandardCharsets.US_ASCII))).isTrue()
Assertions.assertThat(encoder.canEncode(ResolvableType.NONE, null)).isTrue()
}
@Test
override fun encode() {
val input = Flux.just(
Pojo("foo", "bar"),
Pojo("foofoo", "barbar"),
Pojo("foofoofoo", "barbarbar")
)
testEncode(input, Pojo::class.java, { step: FirstStep<DataBuffer?> ->
step
.consumeNextWith(expectString("[" +
"{\"foo\":\"foo\",\"bar\":\"bar\"}," +
"{\"foo\":\"foofoo\",\"bar\":\"barbar\"}," +
"{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}]")
.andThen { dataBuffer: DataBuffer? -> DataBufferUtils.release(dataBuffer) })
.verifyComplete()
})
}
@Test
fun encodeMono() {
val input = Mono.just(Pojo("foo", "bar"))
testEncode(input, Pojo::class.java, { step: FirstStep<DataBuffer?> ->
step
.consumeNextWith(expectString("{\"foo\":\"foo\",\"bar\":\"bar\"}")
.andThen { dataBuffer: DataBuffer? -> DataBufferUtils.release(dataBuffer) })
.verifyComplete()
})
}
@Test
fun canNotEncode() {
Assertions.assertThat(encoder.canEncode(ResolvableType.forClass(String::class.java), null)).isFalse()
Assertions.assertThat(encoder.canEncode(ResolvableType.forClass(Pojo::class.java), MediaType.APPLICATION_XML)).isFalse()
val sseType = ResolvableType.forClass(ServerSentEvent::class.java)
Assertions.assertThat(encoder.canEncode(sseType, MediaType.APPLICATION_JSON)).isFalse()
}
@Serializable
data class Pojo(val foo: String, val bar: String)
}

View File

@@ -389,14 +389,14 @@ project for more details.
=== Kotlin multiplatform serialization
As of Spring Framework 5.3, https://github.com/Kotlin/kotlinx.serialization[Kotlin multiplatform serialization] is
supported in Spring MVC. The builtin support currently only targets JSON format.
supported in Spring MVC and Spring WebFlux. The builtin support currently only targets JSON format.
To enable it, follow https://github.com/Kotlin/kotlinx.serialization#setup[those instructions] and make sure neither
Jackson, GSON or JSONB are in the classpath.
NOTE: For a typical Spring Boot web application, that can be achieved by excluding `spring-boot-starter-json` dependency.
If you need Jackson, GSON or JSONB for other purposes, you can keep them on the classpath and
In Spring MVC, if you need Jackson, GSON or JSONB for other purposes, you can keep them on the classpath and
<<web#mvc-config-message-converters, configure message converters>> to remove `MappingJackson2HttpMessageConverter` and add
`KotlinSerializationJsonHttpMessageConverter`.