Skip to content

Commit

Permalink
Support Kotlin Serialization custom serializers
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sdeleuze committed Aug 4, 2023
1 parent e83793b commit da7b68a
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 16 deletions.
@@ -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.
Expand Down Expand Up @@ -46,8 +46,7 @@
*/
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;

Expand Down Expand Up @@ -119,18 +118,18 @@ private boolean supports(@Nullable MimeType mimeType) {
@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) {
}
if (serializer != null) {
if (hasPolymorphism(serializer.getDescriptor(), new HashSet<>())) {
return null;
}
serializerCache.put(type, serializer);
this.serializerCache.put(type, serializer);
}
}
return serializer;
Expand Down
@@ -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.
Expand Down Expand Up @@ -50,8 +50,7 @@
*/
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;

Expand Down Expand Up @@ -149,18 +148,18 @@ protected abstract void writeInternal(Object object, KSerializer<Object> seriali
*/
@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) {
}
if (serializer != null) {
if (hasPolymorphism(serializer.getDescriptor(), new HashSet<>())) {
return null;
}
serializerCache.put(type, serializer);
this.serializerCache.put(type, serializer);
}
}
return serializer;
Expand Down
@@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -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")
Expand All @@ -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
Expand Down
@@ -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.
Expand Down Expand Up @@ -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

/**
Expand Down Expand Up @@ -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()
}


Expand Down
@@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit da7b68a

Please sign in to comment.