Skip to content

Commit 22bcac1

Browse files
committed
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
1 parent 546ae15 commit 22bcac1

File tree

4 files changed

+86
-10
lines changed

4 files changed

+86
-10
lines changed

spring-test/src/test/kotlin/org/springframework/test/web/reactive/server/WebTestClientKotlinTests.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
/*
2+
* Copyright 2002-present 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+
117
package org.springframework.test.web.reactive.server
218

319
import kotlinx.serialization.Serializable
@@ -34,4 +50,4 @@ class WebTestClientKotlinTests {
3450
@GetMapping("test")
3551
fun test(): List<Response> = listOf(Response("Hello"), Response("World"))
3652
}
37-
}
53+
}

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

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public abstract class KotlinSerializationStringDecoder<T extends StringFormat> e
5555
implements Decoder<Object> {
5656

5757
// String decoding needed for now, see https://github.com/Kotlin/kotlinx.serialization/issues/204 for more details
58-
private final StringDecoder stringDecoder = StringDecoder.allMimeTypes(StringDecoder.DEFAULT_DELIMITERS, false);
58+
protected final StringDecoder stringDecoder = StringDecoder.allMimeTypes(StringDecoder.DEFAULT_DELIMITERS, false);
5959

6060

6161
/**
@@ -116,21 +116,23 @@ public List<MimeType> getDecodableMimeTypes(ResolvableType targetType) {
116116
}
117117

118118
@Override
119-
@SuppressWarnings("unchecked")
120119
public Flux<Object> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
121120
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
122121
return Flux.defer(() -> {
123122
KSerializer<Object> serializer = serializer(elementType);
124-
KSerializer<Object> listSerializer = serializer(ResolvableType.forClassWithGenerics(List.class, elementType));
125-
if (serializer == null || listSerializer == null) {
123+
if (serializer == null) {
126124
return Mono.error(new DecodingException("Could not find KSerializer for " + elementType));
127125
}
128126
return this.stringDecoder
129127
.decode(inputStream, elementType, mimeType, hints)
130-
.flatMapIterable(string -> string.startsWith("[") ?
131-
(List<Object>) format().decodeFromString(listSerializer, string) :
132-
List.of(format().decodeFromString(serializer, string)))
133-
.onErrorMap(IllegalArgumentException.class, this::processException);
128+
.handle((string, sink) -> {
129+
try {
130+
sink.next(format().decodeFromString(serializer, string));
131+
}
132+
catch (IllegalArgumentException ex) {
133+
sink.error(processException(ex));
134+
}
135+
});
134136
});
135137
}
136138

@@ -156,7 +158,7 @@ public Mono<Object> decodeToMono(Publisher<DataBuffer> inputStream, ResolvableTy
156158
});
157159
}
158160

159-
private CodecException processException(IllegalArgumentException ex) {
161+
protected CodecException processException(IllegalArgumentException ex) {
160162
return new DecodingException("Decoding error: " + ex.getMessage(), ex);
161163
}
162164

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,22 @@
1616

1717
package org.springframework.http.codec.json;
1818

19+
import java.util.List;
20+
import java.util.Map;
21+
import java.util.Objects;
1922
import java.util.function.Predicate;
2023

24+
import kotlinx.serialization.KSerializer;
25+
import kotlinx.serialization.builtins.BuiltinSerializersKt;
2126
import kotlinx.serialization.json.Json;
27+
import org.jspecify.annotations.Nullable;
28+
import org.reactivestreams.Publisher;
29+
import reactor.core.publisher.Flux;
30+
import reactor.core.publisher.Mono;
2231

2332
import org.springframework.core.ResolvableType;
33+
import org.springframework.core.codec.DecodingException;
34+
import org.springframework.core.io.buffer.DataBuffer;
2435
import org.springframework.http.MediaType;
2536
import org.springframework.http.codec.KotlinSerializationStringDecoder;
2637
import org.springframework.util.MimeType;
@@ -95,4 +106,37 @@ public KotlinSerializationJsonDecoder(Json json, Predicate<ResolvableType> typeP
95106
super(json, typePredicate, DEFAULT_JSON_MIME_TYPES);
96107
}
97108

109+
@Override
110+
public Flux<Object> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
111+
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
112+
return Flux.defer(() -> {
113+
KSerializer<Object> serializer = serializer(elementType);
114+
if (serializer == null) {
115+
return Mono.error(new DecodingException("Could not find KSerializer for " + elementType));
116+
}
117+
return this.stringDecoder
118+
.decode(inputStream, elementType, mimeType, hints)
119+
.switchOnFirst((signal, flux) -> {
120+
if (signal.hasValue()) {
121+
String value = Objects.requireNonNull(signal.get());
122+
if (value.stripLeading().startsWith("[") && !List.class.isAssignableFrom(elementType.toClass())) {
123+
KSerializer<List<Object>> listSerializer = BuiltinSerializersKt.ListSerializer(serializer);
124+
return flux
125+
.flatMapIterable(string -> format().decodeFromString(listSerializer, string))
126+
.onErrorMap(IllegalArgumentException.class, this::processException);
127+
}
128+
return flux.handle((string, sink) -> {
129+
try {
130+
sink.next(format().decodeFromString(serializer, string));
131+
}
132+
catch (IllegalArgumentException ex) {
133+
sink.error(processException(ex));
134+
}
135+
});
136+
}
137+
return flux;
138+
});
139+
});
140+
}
141+
98142
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,20 @@ class KotlinSerializationJsonDecoderTests : AbstractDecoderTests<KotlinSerializa
211211
}, null, null)
212212
}
213213

214+
@Test
215+
fun decodeJsonArrayToFluxOfList() {
216+
val input = Flux.concat(
217+
stringBuffer("[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]\n"),
218+
stringBuffer("[{\"bar\":\"b3\",\"foo\":\"f3\"},{\"bar\":\"b4\",\"foo\":\"f4\"}]"))
219+
220+
testDecodeAll(input, ResolvableType.forClassWithGenerics(List::class.java, Pojo::class.java), {
221+
it.expectNext(listOf(Pojo("f1", "b1"),Pojo("f2", "b2")))
222+
.expectNext(listOf(Pojo("f3", "b3"),Pojo("f4", "b4")))
223+
.expectComplete()
224+
.verify()
225+
}, null, null)
226+
}
227+
214228
@Test
215229
override fun decodeToMono() {
216230
val input = Flux.concat(

0 commit comments

Comments
 (0)