Merge branch '7.0.x'

This commit is contained in:
Sébastien Deleuze
2026-04-10 16:10:45 +02:00
11 changed files with 158 additions and 9 deletions
@@ -55,7 +55,7 @@ public abstract class KotlinSerializationStringDecoder<T extends StringFormat> e
implements Decoder<Object> {
// 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);
/**
@@ -158,7 +158,7 @@ public abstract class KotlinSerializationStringDecoder<T extends StringFormat> e
});
}
private CodecException processException(IllegalArgumentException ex) {
protected CodecException processException(IllegalArgumentException ex) {
return new DecodingException("Decoding error: " + ex.getMessage(), ex);
}
@@ -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;
@@ -96,4 +107,37 @@ public class KotlinSerializationJsonDecoder extends KotlinSerializationStringDec
super(json, typePredicate, DEFAULT_JSON_MIME_TYPES);
}
@Override
public Flux<Object> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
return Flux.defer(() -> {
KSerializer<Object> 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<List<Object>> 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;
});
});
}
}
@@ -316,7 +316,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
Object arg = args[index];
if (!(parameter.isOptional() && arg == null)) {
KType type = parameter.getType();
if (!(type.isMarkedNullable() && arg == null) &&
if (!type.isMarkedNullable() &&
type.getClassifier() instanceof KClass<?> kClass &&
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) {
arg = box(kClass, arg);
@@ -337,7 +337,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
private static Object box(KClass<?> kClass, @Nullable Object arg) {
KFunction<?> constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass));
KType type = constructor.getParameters().get(0).getType();
if (!(type.isMarkedNullable() && arg == null) &&
if (!type.isMarkedNullable() &&
type.getClassifier() instanceof KClass<?> parameterClass &&
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass))) {
arg = box(parameterClass, arg);
@@ -197,6 +197,34 @@ class KotlinSerializationJsonDecoderTests : AbstractDecoderTests<KotlinSerializa
}, null, null)
}
@Test
fun decodeJsonArrayToFlux() {
val input = Flux.concat(
stringBuffer("[{\"bar\":\"b1\",\"foo\":\"f1\"},"),
stringBuffer("{\"bar\":\"b2\",\"foo\":\"f2\"}]"))
testDecodeAll(input, ResolvableType.forClass(Pojo::class.java), {
it.expectNext(Pojo("f1", "b1"))
.expectNext(Pojo("f2", "b2"))
.expectComplete()
.verify()
}, null, null)
}
@Test
fun decodeJsonArrayToFluxOfList() {
val input = Flux.concat(
stringBuffer("[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]\n"),
stringBuffer("[{\"bar\":\"b3\",\"foo\":\"f3\"},{\"bar\":\"b4\",\"foo\":\"f4\"}]"))
testDecodeAll(input, ResolvableType.forClassWithGenerics(List::class.java, Pojo::class.java), {
it.expectNext(listOf(Pojo("f1", "b1"),Pojo("f2", "b2")))
.expectNext(listOf(Pojo("f3", "b3"),Pojo("f4", "b4")))
.expectComplete()
.verify()
}, null, null)
}
@Test
override fun decodeToMono() {
val input = Flux.concat(
@@ -148,6 +148,13 @@ class InvocableHandlerMethodKotlinTests {
Assertions.assertThatIllegalArgumentException().isThrownBy { invocable.invokeForRequest(request, null) }
}
@Test
fun valueClassWithNullableAndNonNullParameter() {
composite.addResolver(StubArgumentResolver(LongValueClass::class.java, LongValueClass(1)))
val value = getInvocable(ValueClassHandler::valueClassWithNullable.javaMethod!!).invokeForRequest(request, null)
Assertions.assertThat(value).isEqualTo(1L)
}
@Test
fun valueClassWithNullable() {
composite.addResolver(StubArgumentResolver(LongValueClass::class.java, null))