Support Kotlin Serialization custom serializers

This commit updates WebMVC converters and WebFlux
encoders/decoders to support custom serializers
with Kotlin Serialization when specified via
a custom SerialFormat.

It also turns the serializers cache to a non-static
field in order to allow per converter/encoder/decoder
configuration.

Closes gh-30870
This commit is contained in:
Sébastien Deleuze 2023-08-04 11:20:08 +02:00
parent e83793ba7f
commit da7b68a643
8 changed files with 237 additions and 16 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@ -46,8 +46,7 @@ import org.springframework.util.MimeType;
*/
public abstract class KotlinSerializationSupport<T extends SerialFormat> {
private static final Map<Type, KSerializer<Object>> serializerCache = new ConcurrentReferenceHashMap<>();
private final Map<Type, KSerializer<Object>> serializerCache = new ConcurrentReferenceHashMap<>();
private final T format;
@ -119,10 +118,10 @@ public abstract class KotlinSerializationSupport<T extends SerialFormat> {
@Nullable
protected final KSerializer<Object> serializer(ResolvableType resolvableType) {
Type type = resolvableType.getType();
KSerializer<Object> serializer = serializerCache.get(type);
KSerializer<Object> serializer = this.serializerCache.get(type);
if (serializer == null) {
try {
serializer = SerializersKt.serializerOrNull(type);
serializer = SerializersKt.serializerOrNull(this.format.getSerializersModule(), type);
}
catch (IllegalArgumentException ignored) {
}
@ -130,7 +129,7 @@ public abstract class KotlinSerializationSupport<T extends SerialFormat> {
if (hasPolymorphism(serializer.getDescriptor(), new HashSet<>())) {
return null;
}
serializerCache.put(type, serializer);
this.serializerCache.put(type, serializer);
}
}
return serializer;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@ -50,8 +50,7 @@ import org.springframework.util.ConcurrentReferenceHashMap;
*/
public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends SerialFormat> extends AbstractGenericHttpMessageConverter<Object> {
private static final Map<Type, KSerializer<Object>> serializerCache = new ConcurrentReferenceHashMap<>();
private final Map<Type, KSerializer<Object>> serializerCache = new ConcurrentReferenceHashMap<>();
private final T format;
@ -149,10 +148,10 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends
*/
@Nullable
private KSerializer<Object> serializer(Type type) {
KSerializer<Object> serializer = serializerCache.get(type);
KSerializer<Object> serializer = this.serializerCache.get(type);
if (serializer == null) {
try {
serializer = SerializersKt.serializerOrNull(type);
serializer = SerializersKt.serializerOrNull(this.format.getSerializersModule(), type);
}
catch (IllegalArgumentException ignored) {
}
@ -160,7 +159,7 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends
if (hasPolymorphism(serializer.getDescriptor(), new HashSet<>())) {
return null;
}
serializerCache.put(type, serializer);
this.serializerCache.put(type, serializer);
}
}
return serializer;

View File

@ -0,0 +1,42 @@
/*
* Copyright 2002-2023 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
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import java.math.BigDecimal
object BigDecimalSerializer : KSerializer<BigDecimal> {
override val descriptor = PrimitiveSerialDescriptor("BigDecimal", PrimitiveKind.DOUBLE)
override fun deserialize(decoder: Decoder): BigDecimal = BigDecimal.valueOf(decoder.decodeDouble())
override fun serialize(encoder: Encoder, value: BigDecimal) {
encoder.encodeDouble(value.toDouble())
}
}
val customJson = Json {
serializersModule = SerializersModule {
contextual(BigDecimal::class, BigDecimalSerializer)
}
}

View File

@ -0,0 +1,81 @@
/*
* Copyright 2002-2023 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 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 org.springframework.http.customJson
import reactor.core.publisher.Mono
import reactor.test.StepVerifier
import java.math.BigDecimal
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
/**
* Tests for the JSON decoding using kotlinx.serialization with a custom serializer module.
*
* @author Sebastien Deleuze
*/
class CustomKotlinSerializationJsonDecoderTests :
AbstractDecoderTests<KotlinSerializationJsonDecoder>(KotlinSerializationJsonDecoder(customJson)) {
@Test
override fun canDecode() {
val bigDecimalType = ResolvableType.forClass(BigDecimal::class.java)
Assertions.assertThat(decoder.canDecode(bigDecimalType, MediaType.APPLICATION_JSON)).isTrue()
}
@Test
override fun decode() {
val output = decoder.decode(Mono.empty(),
ResolvableType.forClass(KotlinSerializationJsonDecoderTests.Pojo::class.java), null, emptyMap())
StepVerifier
.create(output)
.expectError(UnsupportedOperationException::class.java)
.verify()
}
@Test
override fun decodeToMono() {
val input = stringBuffer("1.0")
val output = decoder.decodeToMono(input,
ResolvableType.forClass(BigDecimal::class.java), null, emptyMap())
StepVerifier
.create(output)
.expectNext(BigDecimal.valueOf(1.0))
.expectComplete()
.verify()
}
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)
}
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 2002-2023 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 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.customJson
import reactor.core.publisher.Mono
import reactor.test.StepVerifier
import java.math.BigDecimal
/**
* Tests for the JSON encoding using kotlinx.serialization with a custom serializer module.
*
* @author Sebastien Deleuze
*/
class CustomKotlinSerializationJsonEncoderTests :
AbstractEncoderTests<KotlinSerializationJsonEncoder>(KotlinSerializationJsonEncoder(customJson)) {
@Test
override fun canEncode() {
val bigDecimalType = ResolvableType.forClass(BigDecimal::class.java)
Assertions.assertThat(encoder.canEncode(bigDecimalType, MediaType.APPLICATION_JSON)).isTrue()
}
@Test
override fun encode() {
val input = Mono.just(BigDecimal(1))
testEncode(input, BigDecimal::class.java) { step: StepVerifier.FirstStep<DataBuffer?> ->
step.consumeNextWith(expectString("1.0")
.andThen { dataBuffer: DataBuffer? -> DataBufferUtils.release(dataBuffer) })
.verifyComplete()
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@ -29,6 +29,7 @@ import reactor.core.publisher.Mono
import reactor.test.StepVerifier
import reactor.test.StepVerifier.FirstStep
import java.lang.UnsupportedOperationException
import java.math.BigDecimal
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
@ -39,7 +40,6 @@ import java.nio.charset.StandardCharsets
*/
class KotlinSerializationJsonDecoderTests : AbstractDecoderTests<KotlinSerializationJsonDecoder>(KotlinSerializationJsonDecoder()) {
@Suppress("UsePropertyAccessSyntax", "DEPRECATION")
@Test
override fun canDecode() {
val jsonSubtype = MediaType("application", "vnd.test-micro-type+json")
@ -62,6 +62,7 @@ class KotlinSerializationJsonDecoderTests : AbstractDecoderTests<KotlinSerializa
assertThat(decoder.canDecode(ResolvableType.forClassWithGenerics(ArrayList::class.java, Int::class.java), MediaType.APPLICATION_PDF)).isFalse()
assertThat(decoder.canDecode(ResolvableType.forClass(Ordered::class.java), MediaType.APPLICATION_JSON)).isFalse()
assertThat(decoder.canDecode(ResolvableType.NONE, MediaType.APPLICATION_JSON)).isFalse()
assertThat(decoder.canDecode(ResolvableType.forClass(BigDecimal::class.java), MediaType.APPLICATION_JSON)).isFalse()
}
@Test

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@ -29,6 +29,7 @@ import org.springframework.http.codec.ServerSentEvent
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.test.StepVerifier.FirstStep
import java.math.BigDecimal
import java.nio.charset.StandardCharsets
/**
@ -93,6 +94,7 @@ class KotlinSerializationJsonEncoderTests : AbstractEncoderTests<KotlinSerializa
val sseType = ResolvableType.forClass(ServerSentEvent::class.java)
assertThat(encoder.canEncode(sseType, MediaType.APPLICATION_JSON)).isFalse()
assertThat(encoder.canEncode(ResolvableType.forClass(Ordered::class.java), MediaType.APPLICATION_JSON)).isFalse()
assertThat(encoder.canEncode(ResolvableType.forClass(BigDecimal::class.java), null)).isFalse()
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 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.
@ -31,8 +31,10 @@ import org.springframework.core.Ordered
import org.springframework.core.ResolvableType
import org.springframework.http.MediaType
import org.springframework.http.converter.HttpMessageNotReadableException
import org.springframework.http.customJson
import org.springframework.web.testfixture.http.MockHttpInputMessage
import org.springframework.web.testfixture.http.MockHttpOutputMessage
import java.math.BigDecimal
/**
* Tests for the JSON conversion using kotlinx.serialization.
@ -67,6 +69,8 @@ class KotlinSerializationJsonHttpMessageConverterTests {
assertThat(converter.canRead(typeTokenOf<List<Ordered>>(), List::class.java, MediaType.APPLICATION_JSON)).isFalse()
assertThat(converter.canRead(ResolvableType.NONE.type, null, MediaType.APPLICATION_JSON)).isFalse()
assertThat(converter.canRead(BigDecimal::class.java, null, MediaType.APPLICATION_JSON)).isFalse()
}
@Test
@ -90,6 +94,8 @@ class KotlinSerializationJsonHttpMessageConverterTests {
assertThat(converter.canWrite(typeTokenOf<Ordered>(), Ordered::class.java, MediaType.APPLICATION_JSON)).isFalse()
assertThat(converter.canWrite(ResolvableType.NONE.type, SerializableBean::class.java, MediaType.APPLICATION_JSON)).isFalse()
assertThat(converter.canWrite(BigDecimal::class.java, BigDecimal::class.java, MediaType.APPLICATION_JSON)).isFalse()
}
@Test
@ -314,6 +320,42 @@ class KotlinSerializationJsonHttpMessageConverterTests {
assertThat(result).isEqualTo("\"H\u00e9llo W\u00f6rld\"")
}
@Test
fun canReadBigDecimalWithSerializerModule() {
val customConverter = KotlinSerializationJsonHttpMessageConverter(customJson)
assertThat(customConverter.canRead(BigDecimal::class.java, MediaType.APPLICATION_JSON)).isTrue()
}
@Test
fun canWriteBigDecimalWithSerializerModule() {
val customConverter = KotlinSerializationJsonHttpMessageConverter(customJson)
assertThat(customConverter.canWrite(BigDecimal::class.java, MediaType.APPLICATION_JSON)).isTrue()
}
@Test
fun readBigDecimalWithSerializerModule() {
val body = "1.0"
val inputMessage = MockHttpInputMessage(body.toByteArray(charset("UTF-8")))
inputMessage.headers.contentType = MediaType.APPLICATION_JSON
val customConverter = KotlinSerializationJsonHttpMessageConverter(customJson)
val result = customConverter.read(BigDecimal::class.java, inputMessage) as BigDecimal
assertThat(result).isEqualTo(BigDecimal.valueOf(1.0))
}
@Test
fun writeBigDecimalWithSerializerModule() {
val outputMessage = MockHttpOutputMessage()
val customConverter = KotlinSerializationJsonHttpMessageConverter(customJson)
customConverter.write(BigDecimal(1), null, outputMessage)
val result = outputMessage.getBodyAsString(StandardCharsets.UTF_8)
assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/json"))
assertThat(result).isEqualTo("1.0")
}
@Serializable
@Suppress("ArrayInDataClass")