diff --git a/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationSupport.java b/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationSupport.java index 5d356c8b49dd..792b2b5abc1c 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationSupport.java +++ b/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationSupport.java @@ -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 @@ */ public abstract class KotlinSerializationSupport { - private static final Map> serializerCache = new ConcurrentReferenceHashMap<>(); - + private final Map> serializerCache = new ConcurrentReferenceHashMap<>(); private final T format; @@ -119,10 +118,10 @@ private boolean supports(@Nullable MimeType mimeType) { @Nullable protected final KSerializer serializer(ResolvableType resolvableType) { Type type = resolvableType.getType(); - KSerializer serializer = serializerCache.get(type); + KSerializer 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 @@ protected final KSerializer serializer(ResolvableType resolvableType) { if (hasPolymorphism(serializer.getDescriptor(), new HashSet<>())) { return null; } - serializerCache.put(type, serializer); + this.serializerCache.put(type, serializer); } } return serializer; diff --git a/spring-web/src/main/java/org/springframework/http/converter/AbstractKotlinSerializationHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/AbstractKotlinSerializationHttpMessageConverter.java index 8f047499e0f6..1b6f9fc1c9bd 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/AbstractKotlinSerializationHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/AbstractKotlinSerializationHttpMessageConverter.java @@ -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 @@ */ public abstract class AbstractKotlinSerializationHttpMessageConverter extends AbstractGenericHttpMessageConverter { - private static final Map> serializerCache = new ConcurrentReferenceHashMap<>(); - + private final Map> serializerCache = new ConcurrentReferenceHashMap<>(); private final T format; @@ -149,10 +148,10 @@ protected abstract void writeInternal(Object object, KSerializer seriali */ @Nullable private KSerializer serializer(Type type) { - KSerializer serializer = serializerCache.get(type); + KSerializer 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 @@ private KSerializer serializer(Type type) { if (hasPolymorphism(serializer.getDescriptor(), new HashSet<>())) { return null; } - serializerCache.put(type, serializer); + this.serializerCache.put(type, serializer); } } return serializer; diff --git a/spring-web/src/test/kotlin/org/springframework/http/BigDecimalSupport.kt b/spring-web/src/test/kotlin/org/springframework/http/BigDecimalSupport.kt new file mode 100644 index 000000000000..4d1f12036e49 --- /dev/null +++ b/spring-web/src/test/kotlin/org/springframework/http/BigDecimalSupport.kt @@ -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 { + 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) + } +} diff --git a/spring-web/src/test/kotlin/org/springframework/http/codec/json/CustomKotlinSerializationJsonDecoderTests.kt b/spring-web/src/test/kotlin/org/springframework/http/codec/json/CustomKotlinSerializationJsonDecoderTests.kt new file mode 100644 index 000000000000..8885152820d8 --- /dev/null +++ b/spring-web/src/test/kotlin/org/springframework/http/codec/json/CustomKotlinSerializationJsonDecoderTests.kt @@ -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(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 { + return stringBuffer(value, StandardCharsets.UTF_8) + } + + private fun stringBuffer(value: String, charset: Charset): Mono { + return Mono.defer { + val bytes = value.toByteArray(charset) + val buffer = bufferFactory.allocateBuffer(bytes.size) + buffer.write(bytes) + Mono.just(buffer) + } + } + +} diff --git a/spring-web/src/test/kotlin/org/springframework/http/codec/json/CustomKotlinSerializationJsonEncoderTests.kt b/spring-web/src/test/kotlin/org/springframework/http/codec/json/CustomKotlinSerializationJsonEncoderTests.kt new file mode 100644 index 000000000000..6cbb6e38096c --- /dev/null +++ b/spring-web/src/test/kotlin/org/springframework/http/codec/json/CustomKotlinSerializationJsonEncoderTests.kt @@ -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(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 -> + step.consumeNextWith(expectString("1.0") + .andThen { dataBuffer: DataBuffer? -> DataBufferUtils.release(dataBuffer) }) + .verifyComplete() + } + } + +} diff --git a/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonDecoderTests.kt b/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonDecoderTests.kt index e674873f6d7b..7950a136f83e 100644 --- a/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonDecoderTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonDecoderTests.kt @@ -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()) { - @Suppress("UsePropertyAccessSyntax", "DEPRECATION") @Test override fun canDecode() { val jsonSubtype = MediaType("application", "vnd.test-micro-type+json") @@ -62,6 +62,7 @@ class KotlinSerializationJsonDecoderTests : AbstractDecoderTests>(), 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::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")