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:
parent
e83793ba7f
commit
da7b68a643
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue