diff --git a/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java
index ec850a697c..bc55b86c43 100644
--- a/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java
+++ b/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java
@@ -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
diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java
new file mode 100644
index 0000000000..98da5971f6
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java
@@ -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
+ * kotlinx.serialization .
+ *
+ *
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.
+ *
+ *
Decoding streams is not supported yet, see
+ * kotlinx.serialization/issues/1073
+ * related issue.
+ *
+ * @author Sebastien Deleuze
+ * @since 5.3
+ */
+public class KotlinSerializationJsonDecoder extends AbstractDecoder {
+
+ private static final Map> 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 decode(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Map hints) {
+ return Flux.error(new UnsupportedOperationException());
+ }
+
+ @Override
+ public Mono decodeToMono(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Map 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.
+ * 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 serializer(Type type) {
+ KSerializer serializer = serializerCache.get(type);
+ if (serializer == null) {
+ serializer = SerializersKt.serializer(type);
+ serializerCache.put(type, serializer);
+ }
+ return serializer;
+ }
+}
diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonEncoder.java
new file mode 100644
index 0000000000..f40e684fcc
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonEncoder.java
@@ -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
+ * kotlinx.serialization .
+ *
+ * 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 {
+
+ private static final Map> 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 encode(Publisher> inputStream, DataBufferFactory bufferFactory, ResolvableType elementType, MimeType mimeType, Map 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 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.
+ * 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 serializer(Type type) {
+ KSerializer serializer = serializerCache.get(type);
+ if (serializer == null) {
+ serializer = SerializersKt.serializer(type);
+ serializerCache.put(type, serializer);
+ }
+ return serializer;
+ }
+}
diff --git a/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java b/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java
index b3cb66400e..0f40420298 100644
--- a/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java
+++ b/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java
@@ -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;
+ }
+
}
diff --git a/spring-web/src/main/java/org/springframework/http/codec/support/ClientDefaultCodecsImpl.java b/spring-web/src/main/java/org/springframework/http/codec/support/ClientDefaultCodecsImpl.java
index cc1c7f1a43..1179e27418 100644
--- a/spring-web/src/main/java/org/springframework/http/codec/support/ClientDefaultCodecsImpl.java
+++ b/spring-web/src/main/java/org/springframework/http/codec/support/ClientDefaultCodecsImpl.java
@@ -96,9 +96,10 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo
@Override
protected void extendObjectReaders(List> 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));
}
diff --git a/spring-web/src/main/java/org/springframework/http/codec/support/ServerDefaultCodecsImpl.java b/spring-web/src/main/java/org/springframework/http/codec/support/ServerDefaultCodecsImpl.java
index 9b8de3f068..2a0de1a76e 100644
--- a/spring-web/src/main/java/org/springframework/http/codec/support/ServerDefaultCodecsImpl.java
+++ b/spring-web/src/main/java/org/springframework/http/codec/support/ServerDefaultCodecsImpl.java
@@ -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;
}
}
diff --git a/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonDecoderTests.kt b/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonDecoderTests.kt
new file mode 100644
index 0000000000..6be5d1981c
--- /dev/null
+++ b/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonDecoderTests.kt
@@ -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()) {
+
+ @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 ->
+ step
+ .expectNext(listOf(Pojo("f1", "b1"), Pojo("f2", "b2")))
+ .expectComplete()
+ .verify()
+ }, null, null)
+ }
+
+ private fun stringBuffer(value: String): Mono {
+ return stringBuffer(value, StandardCharsets.UTF_8)
+ }
+
+ private fun stringBuffer(value: String, charset: Charset): Mono {
+ 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)
+
+}
\ No newline at end of file
diff --git a/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonEncoderTests.kt b/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonEncoderTests.kt
new file mode 100644
index 0000000000..d1a6a987d8
--- /dev/null
+++ b/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonEncoderTests.kt
@@ -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()) {
+
+ @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 ->
+ 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 ->
+ 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)
+
+}
\ No newline at end of file
diff --git a/src/docs/asciidoc/languages/kotlin.adoc b/src/docs/asciidoc/languages/kotlin.adoc
index 0285a99204..1acf4bb8cf 100644
--- a/src/docs/asciidoc/languages/kotlin.adoc
+++ b/src/docs/asciidoc/languages/kotlin.adoc
@@ -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
<> to remove `MappingJackson2HttpMessageConverter` and add
`KotlinSerializationJsonHttpMessageConverter`.