Skip to content

Commit da7b68a

Browse files
committed
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
1 parent e83793b commit da7b68a

File tree

8 files changed

+237
-16
lines changed

8 files changed

+237
-16
lines changed

spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationSupport.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -46,8 +46,7 @@
4646
*/
4747
public abstract class KotlinSerializationSupport<T extends SerialFormat> {
4848

49-
private static final Map<Type, KSerializer<Object>> serializerCache = new ConcurrentReferenceHashMap<>();
50-
49+
private final Map<Type, KSerializer<Object>> serializerCache = new ConcurrentReferenceHashMap<>();
5150

5251
private final T format;
5352

@@ -119,18 +118,18 @@ private boolean supports(@Nullable MimeType mimeType) {
119118
@Nullable
120119
protected final KSerializer<Object> serializer(ResolvableType resolvableType) {
121120
Type type = resolvableType.getType();
122-
KSerializer<Object> serializer = serializerCache.get(type);
121+
KSerializer<Object> serializer = this.serializerCache.get(type);
123122
if (serializer == null) {
124123
try {
125-
serializer = SerializersKt.serializerOrNull(type);
124+
serializer = SerializersKt.serializerOrNull(this.format.getSerializersModule(), type);
126125
}
127126
catch (IllegalArgumentException ignored) {
128127
}
129128
if (serializer != null) {
130129
if (hasPolymorphism(serializer.getDescriptor(), new HashSet<>())) {
131130
return null;
132131
}
133-
serializerCache.put(type, serializer);
132+
this.serializerCache.put(type, serializer);
134133
}
135134
}
136135
return serializer;

spring-web/src/main/java/org/springframework/http/converter/AbstractKotlinSerializationHttpMessageConverter.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -50,8 +50,7 @@
5050
*/
5151
public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends SerialFormat> extends AbstractGenericHttpMessageConverter<Object> {
5252

53-
private static final Map<Type, KSerializer<Object>> serializerCache = new ConcurrentReferenceHashMap<>();
54-
53+
private final Map<Type, KSerializer<Object>> serializerCache = new ConcurrentReferenceHashMap<>();
5554

5655
private final T format;
5756

@@ -149,18 +148,18 @@ protected abstract void writeInternal(Object object, KSerializer<Object> seriali
149148
*/
150149
@Nullable
151150
private KSerializer<Object> serializer(Type type) {
152-
KSerializer<Object> serializer = serializerCache.get(type);
151+
KSerializer<Object> serializer = this.serializerCache.get(type);
153152
if (serializer == null) {
154153
try {
155-
serializer = SerializersKt.serializerOrNull(type);
154+
serializer = SerializersKt.serializerOrNull(this.format.getSerializersModule(), type);
156155
}
157156
catch (IllegalArgumentException ignored) {
158157
}
159158
if (serializer != null) {
160159
if (hasPolymorphism(serializer.getDescriptor(), new HashSet<>())) {
161160
return null;
162161
}
163-
serializerCache.put(type, serializer);
162+
this.serializerCache.put(type, serializer);
164163
}
165164
}
166165
return serializer;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http
18+
19+
import kotlinx.serialization.KSerializer
20+
import kotlinx.serialization.descriptors.PrimitiveKind
21+
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
22+
import kotlinx.serialization.encoding.Decoder
23+
import kotlinx.serialization.encoding.Encoder
24+
import kotlinx.serialization.json.Json
25+
import kotlinx.serialization.modules.SerializersModule
26+
import java.math.BigDecimal
27+
28+
object BigDecimalSerializer : KSerializer<BigDecimal> {
29+
override val descriptor = PrimitiveSerialDescriptor("BigDecimal", PrimitiveKind.DOUBLE)
30+
31+
override fun deserialize(decoder: Decoder): BigDecimal = BigDecimal.valueOf(decoder.decodeDouble())
32+
33+
override fun serialize(encoder: Encoder, value: BigDecimal) {
34+
encoder.encodeDouble(value.toDouble())
35+
}
36+
}
37+
38+
val customJson = Json {
39+
serializersModule = SerializersModule {
40+
contextual(BigDecimal::class, BigDecimalSerializer)
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.codec.json
18+
19+
import org.assertj.core.api.Assertions
20+
import org.junit.jupiter.api.Test
21+
import org.springframework.core.ResolvableType
22+
import org.springframework.core.io.buffer.DataBuffer
23+
import org.springframework.core.testfixture.codec.AbstractDecoderTests
24+
import org.springframework.http.MediaType
25+
import org.springframework.http.customJson
26+
import reactor.core.publisher.Mono
27+
import reactor.test.StepVerifier
28+
import java.math.BigDecimal
29+
import java.nio.charset.Charset
30+
import java.nio.charset.StandardCharsets
31+
32+
/**
33+
* Tests for the JSON decoding using kotlinx.serialization with a custom serializer module.
34+
*
35+
* @author Sebastien Deleuze
36+
*/
37+
class CustomKotlinSerializationJsonDecoderTests :
38+
AbstractDecoderTests<KotlinSerializationJsonDecoder>(KotlinSerializationJsonDecoder(customJson)) {
39+
40+
@Test
41+
override fun canDecode() {
42+
val bigDecimalType = ResolvableType.forClass(BigDecimal::class.java)
43+
Assertions.assertThat(decoder.canDecode(bigDecimalType, MediaType.APPLICATION_JSON)).isTrue()
44+
}
45+
46+
@Test
47+
override fun decode() {
48+
val output = decoder.decode(Mono.empty(),
49+
ResolvableType.forClass(KotlinSerializationJsonDecoderTests.Pojo::class.java), null, emptyMap())
50+
StepVerifier
51+
.create(output)
52+
.expectError(UnsupportedOperationException::class.java)
53+
.verify()
54+
}
55+
56+
@Test
57+
override fun decodeToMono() {
58+
val input = stringBuffer("1.0")
59+
val output = decoder.decodeToMono(input,
60+
ResolvableType.forClass(BigDecimal::class.java), null, emptyMap())
61+
StepVerifier
62+
.create(output)
63+
.expectNext(BigDecimal.valueOf(1.0))
64+
.expectComplete()
65+
.verify()
66+
}
67+
68+
private fun stringBuffer(value: String): Mono<DataBuffer> {
69+
return stringBuffer(value, StandardCharsets.UTF_8)
70+
}
71+
72+
private fun stringBuffer(value: String, charset: Charset): Mono<DataBuffer> {
73+
return Mono.defer {
74+
val bytes = value.toByteArray(charset)
75+
val buffer = bufferFactory.allocateBuffer(bytes.size)
76+
buffer.write(bytes)
77+
Mono.just(buffer)
78+
}
79+
}
80+
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.codec.json
18+
19+
import org.assertj.core.api.Assertions
20+
import org.junit.jupiter.api.Test
21+
import org.springframework.core.ResolvableType
22+
import org.springframework.core.io.buffer.DataBuffer
23+
import org.springframework.core.io.buffer.DataBufferUtils
24+
import org.springframework.core.testfixture.codec.AbstractEncoderTests
25+
import org.springframework.http.MediaType
26+
import org.springframework.http.customJson
27+
import reactor.core.publisher.Mono
28+
import reactor.test.StepVerifier
29+
import java.math.BigDecimal
30+
31+
/**
32+
* Tests for the JSON encoding using kotlinx.serialization with a custom serializer module.
33+
*
34+
* @author Sebastien Deleuze
35+
*/
36+
class CustomKotlinSerializationJsonEncoderTests :
37+
AbstractEncoderTests<KotlinSerializationJsonEncoder>(KotlinSerializationJsonEncoder(customJson)) {
38+
39+
@Test
40+
override fun canEncode() {
41+
val bigDecimalType = ResolvableType.forClass(BigDecimal::class.java)
42+
Assertions.assertThat(encoder.canEncode(bigDecimalType, MediaType.APPLICATION_JSON)).isTrue()
43+
}
44+
45+
@Test
46+
override fun encode() {
47+
val input = Mono.just(BigDecimal(1))
48+
testEncode(input, BigDecimal::class.java) { step: StepVerifier.FirstStep<DataBuffer?> ->
49+
step.consumeNextWith(expectString("1.0")
50+
.andThen { dataBuffer: DataBuffer? -> DataBufferUtils.release(dataBuffer) })
51+
.verifyComplete()
52+
}
53+
}
54+
55+
}

spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonDecoderTests.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -29,6 +29,7 @@ import reactor.core.publisher.Mono
2929
import reactor.test.StepVerifier
3030
import reactor.test.StepVerifier.FirstStep
3131
import java.lang.UnsupportedOperationException
32+
import java.math.BigDecimal
3233
import java.nio.charset.Charset
3334
import java.nio.charset.StandardCharsets
3435

@@ -39,7 +40,6 @@ import java.nio.charset.StandardCharsets
3940
*/
4041
class KotlinSerializationJsonDecoderTests : AbstractDecoderTests<KotlinSerializationJsonDecoder>(KotlinSerializationJsonDecoder()) {
4142

42-
@Suppress("UsePropertyAccessSyntax", "DEPRECATION")
4343
@Test
4444
override fun canDecode() {
4545
val jsonSubtype = MediaType("application", "vnd.test-micro-type+json")
@@ -62,6 +62,7 @@ class KotlinSerializationJsonDecoderTests : AbstractDecoderTests<KotlinSerializa
6262
assertThat(decoder.canDecode(ResolvableType.forClassWithGenerics(ArrayList::class.java, Int::class.java), MediaType.APPLICATION_PDF)).isFalse()
6363
assertThat(decoder.canDecode(ResolvableType.forClass(Ordered::class.java), MediaType.APPLICATION_JSON)).isFalse()
6464
assertThat(decoder.canDecode(ResolvableType.NONE, MediaType.APPLICATION_JSON)).isFalse()
65+
assertThat(decoder.canDecode(ResolvableType.forClass(BigDecimal::class.java), MediaType.APPLICATION_JSON)).isFalse()
6566
}
6667

6768
@Test

spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonEncoderTests.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -29,6 +29,7 @@ import org.springframework.http.codec.ServerSentEvent
2929
import reactor.core.publisher.Flux
3030
import reactor.core.publisher.Mono
3131
import reactor.test.StepVerifier.FirstStep
32+
import java.math.BigDecimal
3233
import java.nio.charset.StandardCharsets
3334

3435
/**
@@ -93,6 +94,7 @@ class KotlinSerializationJsonEncoderTests : AbstractEncoderTests<KotlinSerializa
9394
val sseType = ResolvableType.forClass(ServerSentEvent::class.java)
9495
assertThat(encoder.canEncode(sseType, MediaType.APPLICATION_JSON)).isFalse()
9596
assertThat(encoder.canEncode(ResolvableType.forClass(Ordered::class.java), MediaType.APPLICATION_JSON)).isFalse()
97+
assertThat(encoder.canEncode(ResolvableType.forClass(BigDecimal::class.java), null)).isFalse()
9698
}
9799

98100

spring-web/src/test/kotlin/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverterTests.kt

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -31,8 +31,10 @@ import org.springframework.core.Ordered
3131
import org.springframework.core.ResolvableType
3232
import org.springframework.http.MediaType
3333
import org.springframework.http.converter.HttpMessageNotReadableException
34+
import org.springframework.http.customJson
3435
import org.springframework.web.testfixture.http.MockHttpInputMessage
3536
import org.springframework.web.testfixture.http.MockHttpOutputMessage
37+
import java.math.BigDecimal
3638

3739
/**
3840
* Tests for the JSON conversion using kotlinx.serialization.
@@ -67,6 +69,8 @@ class KotlinSerializationJsonHttpMessageConverterTests {
6769
assertThat(converter.canRead(typeTokenOf<List<Ordered>>(), List::class.java, MediaType.APPLICATION_JSON)).isFalse()
6870

6971
assertThat(converter.canRead(ResolvableType.NONE.type, null, MediaType.APPLICATION_JSON)).isFalse()
72+
73+
assertThat(converter.canRead(BigDecimal::class.java, null, MediaType.APPLICATION_JSON)).isFalse()
7074
}
7175

7276
@Test
@@ -90,6 +94,8 @@ class KotlinSerializationJsonHttpMessageConverterTests {
9094
assertThat(converter.canWrite(typeTokenOf<Ordered>(), Ordered::class.java, MediaType.APPLICATION_JSON)).isFalse()
9195

9296
assertThat(converter.canWrite(ResolvableType.NONE.type, SerializableBean::class.java, MediaType.APPLICATION_JSON)).isFalse()
97+
98+
assertThat(converter.canWrite(BigDecimal::class.java, BigDecimal::class.java, MediaType.APPLICATION_JSON)).isFalse()
9399
}
94100

95101
@Test
@@ -314,6 +320,42 @@ class KotlinSerializationJsonHttpMessageConverterTests {
314320
assertThat(result).isEqualTo("\"H\u00e9llo W\u00f6rld\"")
315321
}
316322

323+
@Test
324+
fun canReadBigDecimalWithSerializerModule() {
325+
val customConverter = KotlinSerializationJsonHttpMessageConverter(customJson)
326+
assertThat(customConverter.canRead(BigDecimal::class.java, MediaType.APPLICATION_JSON)).isTrue()
327+
}
328+
329+
@Test
330+
fun canWriteBigDecimalWithSerializerModule() {
331+
val customConverter = KotlinSerializationJsonHttpMessageConverter(customJson)
332+
assertThat(customConverter.canWrite(BigDecimal::class.java, MediaType.APPLICATION_JSON)).isTrue()
333+
}
334+
335+
@Test
336+
fun readBigDecimalWithSerializerModule() {
337+
val body = "1.0"
338+
val inputMessage = MockHttpInputMessage(body.toByteArray(charset("UTF-8")))
339+
inputMessage.headers.contentType = MediaType.APPLICATION_JSON
340+
val customConverter = KotlinSerializationJsonHttpMessageConverter(customJson)
341+
val result = customConverter.read(BigDecimal::class.java, inputMessage) as BigDecimal
342+
343+
assertThat(result).isEqualTo(BigDecimal.valueOf(1.0))
344+
}
345+
346+
@Test
347+
fun writeBigDecimalWithSerializerModule() {
348+
val outputMessage = MockHttpOutputMessage()
349+
val customConverter = KotlinSerializationJsonHttpMessageConverter(customJson)
350+
351+
customConverter.write(BigDecimal(1), null, outputMessage)
352+
353+
val result = outputMessage.getBodyAsString(StandardCharsets.UTF_8)
354+
355+
assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/json"))
356+
assertThat(result).isEqualTo("1.0")
357+
}
358+
317359

318360
@Serializable
319361
@Suppress("ArrayInDataClass")

0 commit comments

Comments
 (0)