From 22bcac1eb5570dc4aac63b25a380c6f07fceeb0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 10 Apr 2026 11:42:41 +0200 Subject: [PATCH] Refine Kotlin serialization contribution This commit moves the JSON specific code to KotlinSerializationJsonDecoder, uses switchOnFirst operator to keep the existing behavior and derives the list serializer from the element one. Closes gh-36597 --- .../server/WebTestClientKotlinTests.kt | 18 +++++++- .../KotlinSerializationStringDecoder.java | 20 +++++---- .../json/KotlinSerializationJsonDecoder.java | 44 +++++++++++++++++++ .../KotlinSerializationJsonDecoderTests.kt | 14 ++++++ 4 files changed, 86 insertions(+), 10 deletions(-) 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 index 69353f358d1..8e22ea41bbd 100644 --- 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 @@ -1,3 +1,19 @@ +/* + * Copyright 2002-present 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.test.web.reactive.server import kotlinx.serialization.Serializable @@ -34,4 +50,4 @@ class WebTestClientKotlinTests { @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 e234ca4c688..69fb226442e 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 @@ -55,7 +55,7 @@ public abstract class KotlinSerializationStringDecoder e implements Decoder { // String decoding needed for now, see https://github.com/Kotlin/kotlinx.serialization/issues/204 for more details - private final StringDecoder stringDecoder = StringDecoder.allMimeTypes(StringDecoder.DEFAULT_DELIMITERS, false); + protected final StringDecoder stringDecoder = StringDecoder.allMimeTypes(StringDecoder.DEFAULT_DELIMITERS, false); /** @@ -116,21 +116,23 @@ public abstract class KotlinSerializationStringDecoder e } @Override - @SuppressWarnings("unchecked") public Flux decode(Publisher inputStream, ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { return Flux.defer(() -> { KSerializer serializer = serializer(elementType); - KSerializer listSerializer = serializer(ResolvableType.forClassWithGenerics(List.class, elementType)); - if (serializer == null || listSerializer == null) { + if (serializer == null) { return Mono.error(new DecodingException("Could not find KSerializer for " + elementType)); } return this.stringDecoder .decode(inputStream, elementType, mimeType, hints) - .flatMapIterable(string -> string.startsWith("[") ? - (List) format().decodeFromString(listSerializer, string) : - List.of(format().decodeFromString(serializer, string))) - .onErrorMap(IllegalArgumentException.class, this::processException); + .handle((string, sink) -> { + try { + sink.next(format().decodeFromString(serializer, string)); + } + catch (IllegalArgumentException ex) { + sink.error(processException(ex)); + } + }); }); } @@ -156,7 +158,7 @@ public abstract class KotlinSerializationStringDecoder e }); } - private CodecException processException(IllegalArgumentException ex) { + protected CodecException processException(IllegalArgumentException ex) { return new DecodingException("Decoding error: " + ex.getMessage(), ex); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java index 227f320987c..7198e1992b5 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java @@ -16,11 +16,22 @@ package org.springframework.http.codec.json; +import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.function.Predicate; +import kotlinx.serialization.KSerializer; +import kotlinx.serialization.builtins.BuiltinSerializersKt; import kotlinx.serialization.json.Json; +import org.jspecify.annotations.Nullable; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.DecodingException; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; import org.springframework.http.codec.KotlinSerializationStringDecoder; import org.springframework.util.MimeType; @@ -95,4 +106,37 @@ public class KotlinSerializationJsonDecoder extends KotlinSerializationStringDec super(json, typePredicate, DEFAULT_JSON_MIME_TYPES); } + @Override + public Flux decode(Publisher inputStream, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + return Flux.defer(() -> { + KSerializer serializer = serializer(elementType); + if (serializer == null) { + return Mono.error(new DecodingException("Could not find KSerializer for " + elementType)); + } + return this.stringDecoder + .decode(inputStream, elementType, mimeType, hints) + .switchOnFirst((signal, flux) -> { + if (signal.hasValue()) { + String value = Objects.requireNonNull(signal.get()); + if (value.stripLeading().startsWith("[") && !List.class.isAssignableFrom(elementType.toClass())) { + KSerializer> listSerializer = BuiltinSerializersKt.ListSerializer(serializer); + return flux + .flatMapIterable(string -> format().decodeFromString(listSerializer, string)) + .onErrorMap(IllegalArgumentException.class, this::processException); + } + return flux.handle((string, sink) -> { + try { + sink.next(format().decodeFromString(serializer, string)); + } + catch (IllegalArgumentException ex) { + sink.error(processException(ex)); + } + }); + } + return flux; + }); + }); + } + } 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 f2e6a93800f..ce53bb9a0ac 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 @@ -211,6 +211,20 @@ class KotlinSerializationJsonDecoderTests : AbstractDecoderTests