From a970fc16aae8b4fd2a923cbb41e6aebbbac016d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= <141109+sdeleuze@users.noreply.github.com> Date: Mon, 27 Jan 2025 14:48:58 +0100 Subject: [PATCH 1/2] Support properties in kotlinx.serialization codecs This commit adds support for Kotlin properties in Spring WebFlux controllers, supported for reasons explained in gh-31856, with kotlinx.serialization codecs. See gh-34284 --- .../codec/KotlinSerializationSupport.java | 31 ++++++++++--------- .../KotlinSerializationJsonEncoderTests.kt | 14 +++++++++ 2 files changed, 30 insertions(+), 15 deletions(-) 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 bded9bb17cb..b3212239cb7 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 @@ -133,24 +133,25 @@ public abstract class KotlinSerializationSupport { Assert.notNull(method, "Method must not be null"); if (KotlinDetector.isKotlinType(method.getDeclaringClass())) { KFunction function = ReflectJvmMapping.getKotlinFunction(method); - Assert.notNull(function, "Kotlin function must not be null"); - KType type = (parameter.getParameterIndex() == -1 ? function.getReturnType() : - KCallables.getValueParameters(function).get(parameter.getParameterIndex()).getType()); - KSerializer serializer = this.kTypeSerializerCache.get(type); - if (serializer == null) { - try { - serializer = SerializersKt.serializerOrNull(this.format.getSerializersModule(), type); - } - catch (IllegalArgumentException ignored) { - } - if (serializer != null) { - if (hasPolymorphism(serializer.getDescriptor(), new HashSet<>())) { - return null; + if (function != null) { + KType type = (parameter.getParameterIndex() == -1 ? function.getReturnType() : + KCallables.getValueParameters(function).get(parameter.getParameterIndex()).getType()); + KSerializer serializer = this.kTypeSerializerCache.get(type); + if (serializer == null) { + try { + serializer = SerializersKt.serializerOrNull(this.format.getSerializersModule(), type); + } + catch (IllegalArgumentException ignored) { + } + if (serializer != null) { + if (hasPolymorphism(serializer.getDescriptor(), new HashSet<>())) { + return null; + } + this.kTypeSerializerCache.put(type, serializer); } - this.kTypeSerializerCache.put(type, serializer); } + return serializer; } - return serializer; } } Type type = resolvableType.getType(); diff --git a/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonEncoderTests.kt b/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonEncoderTests.kt index 86c89a0d160..d5c92147929 100644 --- a/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonEncoderTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonEncoderTests.kt @@ -145,10 +145,24 @@ class KotlinSerializationJsonEncoderTests : AbstractEncoderTests) = map + val value: Int + get() = 42 + } From 5499878de09b925360e9456f3839c307fca3e18f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= <141109+sdeleuze@users.noreply.github.com> Date: Mon, 27 Jan 2025 14:49:31 +0100 Subject: [PATCH 2/2] Support properties in kotlinx.serialization converters This commit adds support for Kotlin properties in Spring WebMVC controllers, supported for reasons explained in gh-31856, with kotlinx.serialization converters. Closes gh-34284 --- ...tlinSerializationHttpMessageConverter.java | 31 ++++++++++--------- ...ializationJsonHttpMessageConverterTests.kt | 27 +++++++++++----- 2 files changed, 36 insertions(+), 22 deletions(-) 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 e5a17226f65..e429ae5fe03 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 @@ -150,24 +150,25 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter function = ReflectJvmMapping.getKotlinFunction(method); - Assert.notNull(function, "Kotlin function must not be null"); - KType type = (parameter.getParameterIndex() == -1 ? function.getReturnType() : - KCallables.getValueParameters(function).get(parameter.getParameterIndex()).getType()); - KSerializer serializer = this.kTypeSerializerCache.get(type); - if (serializer == null) { - try { - serializer = SerializersKt.serializerOrNull(this.format.getSerializersModule(), type); - } - catch (IllegalArgumentException ignored) { - } - if (serializer != null) { - if (hasPolymorphism(serializer.getDescriptor(), new HashSet<>())) { - return null; + if (function != null) { + KType type = (parameter.getParameterIndex() == -1 ? function.getReturnType() : + KCallables.getValueParameters(function).get(parameter.getParameterIndex()).getType()); + KSerializer serializer = this.kTypeSerializerCache.get(type); + if (serializer == null) { + try { + serializer = SerializersKt.serializerOrNull(this.format.getSerializersModule(), type); + } + catch (IllegalArgumentException ignored) { + } + if (serializer != null) { + if (hasPolymorphism(serializer.getDescriptor(), new HashSet<>())) { + return null; + } + this.kTypeSerializerCache.put(type, serializer); } - this.kTypeSerializerCache.put(type, serializer); } + return serializer; } - return serializer; } } Type type = resolvableType.getType(); diff --git a/spring-web/src/test/kotlin/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverterTests.kt b/spring-web/src/test/kotlin/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverterTests.kt index e90047201fe..6bc08fb96f2 100644 --- a/spring-web/src/test/kotlin/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverterTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverterTests.kt @@ -16,18 +16,11 @@ package org.springframework.http.converter.json -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type -import java.nio.charset.StandardCharsets - import kotlinx.serialization.Serializable import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.jupiter.api.Test import org.springframework.core.MethodParameter -import kotlin.reflect.javaType -import kotlin.reflect.typeOf - import org.springframework.core.Ordered import org.springframework.core.ResolvableType import org.springframework.http.MediaType @@ -35,8 +28,12 @@ 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.lang.reflect.ParameterizedType import java.math.BigDecimal +import java.nio.charset.StandardCharsets +import kotlin.reflect.javaType import kotlin.reflect.jvm.javaMethod +import kotlin.reflect.typeOf /** * Tests for the JSON conversion using kotlinx.serialization. @@ -388,6 +385,19 @@ class KotlinSerializationJsonHttpMessageConverterTests { assertThat(result).isEqualTo(expectedJson) } + @Test + fun writeProperty() { + val outputMessage = MockHttpOutputMessage() + val method = this::class.java.getDeclaredMethod("getValue") + val methodParameter = MethodParameter.forExecutable(method, -1) + + this.converter.write(value, ResolvableType.forMethodParameter(methodParameter), null, outputMessage, null) + val result = outputMessage.getBodyAsString(StandardCharsets.UTF_8) + + assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/json")) + assertThat(result).isEqualTo("42") + } + @Serializable @Suppress("ArrayInDataClass") @@ -413,4 +423,7 @@ class KotlinSerializationJsonHttpMessageConverterTests { fun handleMapWithNullable(map: Map) = map + val value: Int + get() = 42 + }