From 1453c690c9c5449699c019be6ece47d4e400eb8a Mon Sep 17 00:00:00 2001 From: Dmitry Sulman Date: Sat, 4 Apr 2026 14:16:43 +0300 Subject: [PATCH] Support JSON list deserialization in KotlinSerializationStringDecoder See #36187 Signed-off-by: Dmitry Sulman --- spring-test/spring-test.gradle | 2 + .../server/WebTestClientKotlinTests.kt | 37 +++++++++++++++++++ .../KotlinSerializationStringDecoder.java | 16 ++++---- .../KotlinSerializationJsonDecoderTests.kt | 14 +++++++ 4 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 spring-test/src/test/kotlin/org/springframework/test/web/reactive/server/WebTestClientKotlinTests.kt diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle index ce8e3c6bd3f9..58284a65fd37 100644 --- a/spring-test/spring-test.gradle +++ b/spring-test/spring-test.gradle @@ -1,6 +1,7 @@ description = "Spring TestContext Framework" apply plugin: "kotlin" +apply plugin: "kotlinx-serialization" dependencies { api(project(":spring-core")) @@ -81,6 +82,7 @@ dependencies { testImplementation("org.hibernate.orm:hibernate-core") testImplementation("org.hibernate.validator:hibernate-validator") testImplementation("org.hsqldb:hsqldb") + testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json") testImplementation("org.junit.platform:junit-platform-testkit") testImplementation("tools.jackson.core:jackson-databind") testRuntimeOnly("com.sun.xml.bind:jaxb-core") diff --git a/spring-test/src/test/kotlin/org/springframework/test/web/reactive/server/WebTestClientKotlinTests.kt b/spring-test/src/test/kotlin/org/springframework/test/web/reactive/server/WebTestClientKotlinTests.kt new file mode 100644 index 000000000000..69353f358d1e --- /dev/null +++ b/spring-test/src/test/kotlin/org/springframework/test/web/reactive/server/WebTestClientKotlinTests.kt @@ -0,0 +1,37 @@ +package org.springframework.test.web.reactive.server + +import kotlinx.serialization.Serializable +import org.junit.jupiter.api.Test +import org.springframework.http.MediaType +import org.springframework.http.codec.json.KotlinSerializationJsonDecoder +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController + +class WebTestClientKotlinTests { + + @Test + fun expectBodyListKotlinSerialization() { + val client = WebTestClient.bindToController(TestController::class.java) + .configureClient() + .codecs { + it.registerDefaults(false) + it.customCodecs().register(KotlinSerializationJsonDecoder()) + }.build() + + client.get().uri("/test") + .accept(MediaType.APPLICATION_JSON) + .exchangeSuccessfully() + .expectBodyList() + .hasSize(2) + .contains(Response("Hello"), Response("World")) + } + + @Serializable + data class Response(val message: String) + + @RestController + class TestController { + @GetMapping("test") + fun test(): List = listOf(Response("Hello"), Response("World")) + } +} \ No newline at end of file diff --git a/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationStringDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationStringDecoder.java index 2529a81997ad..e234ca4c688f 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationStringDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationStringDecoder.java @@ -116,23 +116,21 @@ public List getDecodableMimeTypes(ResolvableType targetType) { } @Override + @SuppressWarnings("unchecked") public Flux decode(Publisher inputStream, ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { return Flux.defer(() -> { KSerializer serializer = serializer(elementType); - if (serializer == null) { + KSerializer listSerializer = serializer(ResolvableType.forClassWithGenerics(List.class, elementType)); + if (serializer == null || listSerializer == null) { return Mono.error(new DecodingException("Could not find KSerializer for " + elementType)); } return this.stringDecoder .decode(inputStream, elementType, mimeType, hints) - .handle((string, sink) -> { - try { - sink.next(format().decodeFromString(serializer, string)); - } - catch (IllegalArgumentException ex) { - sink.error(processException(ex)); - } - }); + .flatMapIterable(string -> string.startsWith("[") ? + (List) format().decodeFromString(listSerializer, string) : + List.of(format().decodeFromString(serializer, string))) + .onErrorMap(IllegalArgumentException.class, this::processException); }); } 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 28c8f87ec5bf..f2e6a93800fb 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 @@ -197,6 +197,20 @@ class KotlinSerializationJsonDecoderTests : AbstractDecoderTests