diff --git a/build.gradle b/build.gradle
index f25893cd4b9..59e19a37769 100644
--- a/build.gradle
+++ b/build.gradle
@@ -8,6 +8,7 @@ plugins {
id "io.freefair.aspectj" version '5.1.1' apply false
id "com.github.ben-manes.versions" version '0.28.0'
id "me.champeau.gradle.jmh" version "0.5.0" apply false
+ id "org.jetbrains.kotlin.plugin.serialization" version "1.4.0" apply false
}
ext {
@@ -87,6 +88,9 @@ configure(allprojects) { project ->
}
dependency "org.ogce:xpp3:1.1.6"
dependency "org.yaml:snakeyaml:1.26"
+ dependencySet(group: 'org.jetbrains.kotlinx', version: '1.0.0-RC') {
+ entry 'kotlinx-serialization-core'
+ }
dependency "com.h2database:h2:1.4.200"
dependency "com.github.ben-manes.caffeine:caffeine:2.8.5"
diff --git a/spring-web/spring-web.gradle b/spring-web/spring-web.gradle
index 9b5c351e7c8..3ab5eddabe4 100644
--- a/spring-web/spring-web.gradle
+++ b/spring-web/spring-web.gradle
@@ -1,6 +1,7 @@
description = "Spring Web"
apply plugin: "kotlin"
+apply plugin: "kotlinx-serialization"
dependencies {
compile(project(":spring-beans"))
@@ -54,6 +55,7 @@ dependencies {
optional("org.codehaus.groovy:groovy")
optional("org.jetbrains.kotlin:kotlin-reflect")
optional("org.jetbrains.kotlin:kotlin-stdlib")
+ optional("org.jetbrains.kotlinx:kotlinx-serialization-core")
testCompile(testFixtures(project(":spring-beans")))
testCompile(testFixtures(project(":spring-context")))
testCompile(testFixtures(project(":spring-core")))
diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java
new file mode 100644
index 00000000000..8db1830fa18
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java
@@ -0,0 +1,154 @@
+/*
+ * 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.converter.json;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+import kotlinx.serialization.KSerializer;
+import kotlinx.serialization.SerializationException;
+import kotlinx.serialization.SerializersKt;
+import kotlinx.serialization.json.Json;
+
+import org.springframework.http.HttpInputMessage;
+import org.springframework.http.HttpOutputMessage;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.AbstractGenericHttpMessageConverter;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.http.converter.HttpMessageNotWritableException;
+import org.springframework.lang.Nullable;
+import org.springframework.util.ConcurrentReferenceHashMap;
+import org.springframework.util.StreamUtils;
+
+/**
+ * Implementation of {@link org.springframework.http.converter.HttpMessageConverter} that can read and write JSON using
+ * kotlinx.serialization .
+ *
+ *
This converter 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 Andreas Ahlenstorf
+ * @author Sebastien Deleuze
+ * @since 5.3
+ */
+public class KotlinSerializationJsonHttpMessageConverter extends AbstractGenericHttpMessageConverter {
+
+ private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
+
+ private static final Map> serializerCache = new ConcurrentReferenceHashMap<>();
+
+ private final Json json;
+
+ /**
+ * Construct a new {@code KotlinSerializationJsonHttpMessageConverter} with the default configuration.
+ */
+ public KotlinSerializationJsonHttpMessageConverter() {
+ this(Json.Default);
+ }
+
+ /**
+ * Construct a new {@code KotlinSerializationJsonHttpMessageConverter} with a custom configuration.
+ */
+ public KotlinSerializationJsonHttpMessageConverter(Json json) {
+ super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
+ this.json = json;
+ }
+
+ @Override
+ protected boolean supports(Class> clazz) {
+ try {
+ resolve(clazz);
+ return true;
+ }
+ catch (Exception ex) {
+ return false;
+ }
+ }
+
+ @Override
+ protected Object readInternal(Class> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
+ return this.read(clazz, null, inputMessage);
+ }
+
+ @Override
+ public Object read(Type type, @Nullable Class> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
+ MediaType contentType = inputMessage.getHeaders().getContentType();
+ String jsonText = StreamUtils.copyToString(inputMessage.getBody(), getCharsetToUse(contentType));
+ try {
+ // TODO Use stream based API when available
+ return this.json.decodeFromString(resolve(type), jsonText);
+ }
+ catch (SerializationException ex) {
+ throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex, inputMessage);
+ }
+ }
+
+ @Override
+ protected void writeInternal(Object o, HttpOutputMessage outputMessage) throws HttpMessageNotWritableException {
+ try {
+ this.writeInternal(o, o.getClass(), outputMessage);
+ }
+ catch (IOException ex) {
+ throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
+ }
+ }
+
+ @Override
+ protected void writeInternal(Object o, @Nullable Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
+ try {
+ String json = this.json.encodeToString(resolve(type), o);
+ MediaType contentType = outputMessage.getHeaders().getContentType();
+ outputMessage.getBody().write(json.getBytes(getCharsetToUse(contentType)));
+ outputMessage.getBody().flush();
+ }
+ catch (IOException ex) {
+ throw ex;
+ }
+ catch (Exception ex) {
+ throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
+ }
+ }
+
+ private Charset getCharsetToUse(@Nullable MediaType contentType) {
+ if (contentType != null && contentType.getCharset() != null) {
+ return contentType.getCharset();
+ }
+ return DEFAULT_CHARSET;
+ }
+
+ /**
+ * 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 to find a serializer for.
+ * @return resolved serializer for the given type.
+ * @throws RuntimeException if no serializer supporting the given type can be found.
+ */
+ private KSerializer resolve(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/converter/support/AllEncompassingFormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java
index e626804d768..78d64e5da47 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java
@@ -20,6 +20,7 @@ import org.springframework.core.SpringProperties;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
+import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
@@ -57,6 +58,8 @@ public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConv
private static final boolean jsonbPresent;
+ private static final boolean kotlinSerializationJsonPresent;
+
static {
ClassLoader classLoader = AllEncompassingFormHttpMessageConverter.class.getClassLoader();
jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);
@@ -66,6 +69,7 @@ public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConv
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
+ kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader);
}
@@ -92,6 +96,9 @@ public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConv
else if (jsonbPresent) {
addPartConverter(new JsonbHttpMessageConverter());
}
+ else if (kotlinSerializationJsonPresent) {
+ addPartConverter(new KotlinSerializationJsonHttpMessageConverter());
+ }
if (jackson2XmlPresent && !shouldIgnoreXml) {
addPartConverter(new MappingJackson2XmlHttpMessageConverter());
diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
index 3170f800e5d..b35efde97e2 100644
--- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
+++ b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
@@ -50,6 +50,7 @@ import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter;
import org.springframework.http.converter.feed.RssChannelHttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
+import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
@@ -114,6 +115,8 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
private static final boolean jsonbPresent;
+ private static final boolean kotlinSerializationJsonPresent;
+
static {
ClassLoader classLoader = RestTemplate.class.getClassLoader();
romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader);
@@ -126,6 +129,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
+ kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader);
}
@@ -179,6 +183,9 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
else if (jsonbPresent) {
this.messageConverters.add(new JsonbHttpMessageConverter());
}
+ else if (kotlinSerializationJsonPresent) {
+ this.messageConverters.add(new KotlinSerializationJsonHttpMessageConverter());
+ }
if (jackson2SmilePresent) {
this.messageConverters.add(new MappingJackson2SmileHttpMessageConverter());
diff --git a/spring-web/src/test/kotlin/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverterTests.kt b/spring-web/src/test/kotlin/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverterTests.kt
new file mode 100644
index 00000000000..a2bf98c4b99
--- /dev/null
+++ b/spring-web/src/test/kotlin/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverterTests.kt
@@ -0,0 +1,299 @@
+/*
+ * 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.converter.json
+
+import kotlinx.serialization.Serializable
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatExceptionOfType
+import org.junit.jupiter.api.Test
+import org.springframework.http.MediaType
+import org.springframework.http.MockHttpInputMessage
+import org.springframework.http.MockHttpOutputMessage
+import org.springframework.http.converter.HttpMessageNotReadableException
+import java.nio.charset.StandardCharsets
+import kotlin.reflect.javaType
+import kotlin.reflect.typeOf
+
+/**
+ * Tests for the JSON conversion using kotlinx.serialization.
+ *
+ * @author Andreas Ahlenstorf
+ * @author Sebastien Deleuze
+ */
+class KotlinSerializationJsonHttpMessageConverterTests {
+
+ private val converter = KotlinSerializationJsonHttpMessageConverter()
+
+ @Test
+ fun canReadJson() {
+ assertThat(converter.canRead(SerializableBean::class.java, MediaType.APPLICATION_JSON)).isTrue()
+ assertThat(converter.canRead(SerializableBean::class.java, MediaType.APPLICATION_PDF)).isFalse()
+ assertThat(converter.canRead(String::class.java, MediaType.APPLICATION_JSON)).isTrue()
+ assertThat(converter.canRead(NotSerializableBean::class.java, MediaType.APPLICATION_JSON)).isFalse()
+
+ assertThat(converter.canRead(Map::class.java, MediaType.APPLICATION_JSON)).isTrue()
+ assertThat(converter.canRead(List::class.java, MediaType.APPLICATION_JSON)).isTrue()
+ assertThat(converter.canRead(Set::class.java, MediaType.APPLICATION_JSON)).isTrue()
+ }
+
+ @Test
+ fun canWriteJson() {
+ assertThat(converter.canWrite(SerializableBean::class.java, MediaType.APPLICATION_JSON)).isTrue()
+ assertThat(converter.canWrite(SerializableBean::class.java, MediaType.APPLICATION_PDF)).isFalse()
+ assertThat(converter.canWrite(String::class.java, MediaType.APPLICATION_JSON)).isTrue()
+ assertThat(converter.canWrite(NotSerializableBean::class.java, MediaType.APPLICATION_JSON)).isFalse()
+
+ assertThat(converter.canWrite(Map::class.java, MediaType.APPLICATION_JSON)).isTrue()
+ assertThat(converter.canWrite(List::class.java, MediaType.APPLICATION_JSON)).isTrue()
+ assertThat(converter.canWrite(Set::class.java, MediaType.APPLICATION_JSON)).isTrue()
+ }
+
+ @Test
+ fun canReadMicroformats() {
+ val jsonSubtype = MediaType("application", "vnd.test-micro-type+json")
+ assertThat(converter.canRead(SerializableBean::class.java, jsonSubtype)).isTrue()
+ }
+
+ @Test
+ fun canWriteMicroformats() {
+ val jsonSubtype = MediaType("application", "vnd.test-micro-type+json")
+ assertThat(converter.canWrite(SerializableBean::class.java, jsonSubtype)).isTrue()
+ }
+
+ @Test
+ fun readObject() {
+ val body = """
+ {
+ "bytes": [
+ 1,
+ 2
+ ],
+ "array": [
+ "Foo",
+ "Bar"
+ ],
+ "number": 42,
+ "string": "Foo",
+ "bool": true,
+ "fraction": 42
+ }
+ """.trimIndent()
+ val inputMessage = MockHttpInputMessage(body.toByteArray(charset("UTF-8")))
+ inputMessage.headers.contentType = MediaType.APPLICATION_JSON
+ val result = converter.read(SerializableBean::class.java, inputMessage) as SerializableBean
+
+ assertThat(result.bytes).containsExactly(0x1, 0x2)
+ assertThat(result.array).containsExactly("Foo", "Bar")
+ assertThat(result.number).isEqualTo(42)
+ assertThat(result.string).isEqualTo("Foo")
+ assertThat(result.bool).isTrue()
+ assertThat(result.fraction).isEqualTo(42.0f)
+ }
+
+ @Test
+ @Suppress("UNCHECKED_CAST")
+ fun readArrayOfObjects() {
+ val body = """
+ [
+ {
+ "bytes": [
+ 1,
+ 2
+ ],
+ "array": [
+ "Foo",
+ "Bar"
+ ],
+ "number": 42,
+ "string": "Foo",
+ "bool": true,
+ "fraction": 42
+ }
+ ]
+ """.trimIndent()
+ val inputMessage = MockHttpInputMessage(body.toByteArray(charset("UTF-8")))
+ inputMessage.headers.contentType = MediaType.APPLICATION_JSON
+ val result = converter.read(Array::class.java, inputMessage) as Array
+
+ assertThat(result).hasSize(1)
+ assertThat(result[0].bytes).containsExactly(0x1, 0x2)
+ assertThat(result[0].array).containsExactly("Foo", "Bar")
+ assertThat(result[0].number).isEqualTo(42)
+ assertThat(result[0].string).isEqualTo("Foo")
+ assertThat(result[0].bool).isTrue()
+ assertThat(result[0].fraction).isEqualTo(42.0f)
+ }
+
+ @Test
+ @Suppress("UNCHECKED_CAST")
+ @ExperimentalStdlibApi
+ fun readGenericCollection() {
+ val body = """
+ [
+ {
+ "bytes": [
+ 1,
+ 2
+ ],
+ "array": [
+ "Foo",
+ "Bar"
+ ],
+ "number": 42,
+ "string": "Foo",
+ "bool": true,
+ "fraction": 42
+ }
+ ]
+ """.trimIndent()
+ val inputMessage = MockHttpInputMessage(body.toByteArray(charset("UTF-8")))
+ inputMessage.headers.contentType = MediaType.APPLICATION_JSON
+ val result = converter.read(typeOf>().javaType, null, inputMessage)
+ as List
+
+ assertThat(result).hasSize(1)
+ assertThat(result[0].bytes).containsExactly(0x1, 0x2)
+ assertThat(result[0].array).containsExactly("Foo", "Bar")
+ assertThat(result[0].number).isEqualTo(42)
+ assertThat(result[0].string).isEqualTo("Foo")
+ assertThat(result[0].bool).isTrue()
+ assertThat(result[0].fraction).isEqualTo(42.0f)
+ }
+
+ @Test
+ fun readObjectInUtf16() {
+ val body = "\"H\u00e9llo W\u00f6rld\""
+ val inputMessage = MockHttpInputMessage(body.toByteArray(StandardCharsets.UTF_16BE))
+ inputMessage.headers.contentType = MediaType("application", "json", StandardCharsets.UTF_16BE)
+
+ val result = this.converter.read(String::class.java, inputMessage)
+
+ assertThat(result).isEqualTo("H\u00e9llo W\u00f6rld")
+ }
+
+ @Test
+ fun readFailsOnInvalidJson() {
+ val body = """
+ this is an invalid JSON document
+ """.trimIndent()
+
+ val inputMessage = MockHttpInputMessage(body.toByteArray(StandardCharsets.UTF_8))
+ inputMessage.headers.contentType = MediaType.APPLICATION_JSON
+ assertThatExceptionOfType(HttpMessageNotReadableException::class.java).isThrownBy {
+ converter.read(SerializableBean::class.java, inputMessage)
+ }
+ }
+
+ @Test
+ fun writeObject() {
+ val outputMessage = MockHttpOutputMessage()
+ val serializableBean = SerializableBean(byteArrayOf(0x1, 0x2), arrayOf("Foo", "Bar"), 42, "Foo", true, 42.0f)
+
+ this.converter.write(serializableBean, null, outputMessage)
+
+ val result = outputMessage.getBodyAsString(StandardCharsets.UTF_8)
+
+ assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/json"))
+ assertThat(result)
+ .contains("\"bytes\":[1,2]")
+ .contains("\"array\":[\"Foo\",\"Bar\"]")
+ .contains("\"number\":42")
+ .contains("\"string\":\"Foo\"")
+ .contains("\"bool\":true")
+ .contains("\"fraction\":42.0")
+ }
+
+ @Test
+ fun writeObjectWithNullableProperty() {
+ val outputMessage = MockHttpOutputMessage()
+ val serializableBean = SerializableBean(byteArrayOf(0x1, 0x2), arrayOf("Foo", "Bar"), 42, null, true, 42.0f)
+
+ this.converter.write(serializableBean, null, outputMessage)
+
+ val result = outputMessage.getBodyAsString(StandardCharsets.UTF_8)
+
+ assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/json"))
+ assertThat(result)
+ .contains("\"bytes\":[1,2]")
+ .contains("\"array\":[\"Foo\",\"Bar\"]")
+ .contains("\"number\":42")
+ .contains("\"string\":null")
+ .contains("\"bool\":true")
+ .contains("\"fraction\":42.0")
+ }
+
+ @Test
+ fun writeArrayOfObjects() {
+ val outputMessage = MockHttpOutputMessage()
+ val serializableBean = SerializableBean(byteArrayOf(0x1, 0x2), arrayOf("Foo", "Bar"), 42, "Foo", true, 42.0f)
+ val expectedJson = """
+ [{"bytes":[1,2],"array":["Foo","Bar"],"number":42,"string":"Foo","bool":true,"fraction":42.0}]
+ """.trimIndent()
+
+ this.converter.write(arrayOf(serializableBean), null, outputMessage)
+
+ val result = outputMessage.getBodyAsString(StandardCharsets.UTF_8)
+
+ assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/json"))
+ assertThat(result).isEqualTo(expectedJson)
+ }
+
+ @Test
+ @ExperimentalStdlibApi
+ fun writeGenericCollection() {
+ val outputMessage = MockHttpOutputMessage()
+ val serializableBean = SerializableBean(byteArrayOf(0x1, 0x2), arrayOf("Foo", "Bar"), 42, "Foo", true, 42.0f)
+ val expectedJson = """
+ [{"bytes":[1,2],"array":["Foo","Bar"],"number":42,"string":"Foo","bool":true,"fraction":42.0}]
+ """.trimIndent()
+
+ this.converter.write(arrayListOf(serializableBean), typeOf>().javaType, null,
+ outputMessage)
+
+ val result = outputMessage.getBodyAsString(StandardCharsets.UTF_8)
+
+ assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/json"))
+ assertThat(result).isEqualTo(expectedJson)
+ }
+
+ @Test
+ fun writeObjectInUtf16() {
+ val outputMessage = MockHttpOutputMessage()
+ val utf16 = "H\u00e9llo W\u00f6rld"
+ val contentType = MediaType("application", "json", StandardCharsets.UTF_16BE)
+
+ this.converter.write(utf16, contentType, outputMessage)
+
+ val result = outputMessage.getBodyAsString(StandardCharsets.UTF_16BE)
+
+ assertThat(outputMessage.headers).containsEntry("Content-Type", listOf(contentType.toString()))
+ assertThat(result).isEqualTo("\"H\u00e9llo W\u00f6rld\"")
+ }
+
+ @Serializable
+ @Suppress("ArrayInDataClass")
+ data class SerializableBean(
+ val bytes: ByteArray,
+ val array: Array,
+ val number: Int,
+ val string: String?,
+ val bool: Boolean,
+ val fraction: Float
+ )
+
+ data class NotSerializableBean(val string: String)
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java
index 5bcdbe4ae54..535ec014daa 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java
@@ -52,6 +52,7 @@ import org.springframework.http.converter.feed.RssChannelHttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
+import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
@@ -208,6 +209,8 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
private static final boolean jsonbPresent;
+ private static final boolean kotlinSerializationJsonPresent;
+
static {
ClassLoader classLoader = WebMvcConfigurationSupport.class.getClassLoader();
romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader);
@@ -219,6 +222,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
+ kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader);
}
@@ -914,6 +918,9 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
else if (jsonbPresent) {
messageConverters.add(new JsonbHttpMessageConverter());
}
+ else if (kotlinSerializationJsonPresent) {
+ messageConverters.add(new KotlinSerializationJsonHttpMessageConverter());
+ }
if (jackson2SmilePresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile();
diff --git a/src/docs/asciidoc/languages/kotlin.adoc b/src/docs/asciidoc/languages/kotlin.adoc
index 629e0fee0ad..0285a99204b 100644
--- a/src/docs/asciidoc/languages/kotlin.adoc
+++ b/src/docs/asciidoc/languages/kotlin.adoc
@@ -383,10 +383,24 @@ class KotlinScriptConfiguration {
}
----
-
See the https://github.com/sdeleuze/kotlin-script-templating[kotlin-script-templating] example
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.
+
+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
+<> to remove `MappingJackson2HttpMessageConverter` and add
+`KotlinSerializationJsonHttpMessageConverter`.
+
+
== Coroutines
Kotlin https://kotlinlang.org/docs/reference/coroutines-overview.html[Coroutines] are Kotlin