From 6bdb9bb950cbe76ca83ba1a5522cc48796555959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= <141109+sdeleuze@users.noreply.github.com> Date: Mon, 17 Feb 2025 15:47:30 +0100 Subject: [PATCH] Support open polymorphism with Kotlin Serialization This commit introduces open polymorphism support with Kotlin Serialization in HTTP converters and codecs as a follow-up of gh-34410 which considers Kotlin Serialization as a Jackson/Gson/Jsonb equivalent (it is not anymore configured before Jackson). Closes gh-34433 --- .../KotlinSerializationBinaryDecoder.java | 6 +- .../KotlinSerializationBinaryEncoder.java | 6 +- .../KotlinSerializationStringDecoder.java | 6 +- .../KotlinSerializationStringEncoder.java | 6 +- .../codec/KotlinSerializationSupport.java | 31 ++------ .../cbor/KotlinSerializationCborDecoder.java | 10 +-- .../cbor/KotlinSerializationCborEncoder.java | 10 +-- .../json/KotlinSerializationJsonDecoder.java | 10 +-- .../json/KotlinSerializationJsonEncoder.java | 10 +-- .../KotlinSerializationProtobufDecoder.java | 10 +-- .../KotlinSerializationProtobufEncoder.java | 10 +-- ...tlinSerializationHttpMessageConverter.java | 30 ++------ ...rializationBinaryHttpMessageConverter.java | 6 +- ...rializationStringHttpMessageConverter.java | 6 +- ...SerializationCborHttpMessageConverter.java | 10 +-- ...SerializationJsonHttpMessageConverter.java | 10 +-- ...alizationProtobufHttpMessageConverter.java | 10 +-- .../KotlinSerializationCborDecoderTests.kt | 21 +++-- .../KotlinSerializationCborEncoderTests.kt | 14 +++- .../KotlinSerializationJsonDecoderTests.kt | 50 +++++++++++- .../KotlinSerializationJsonEncoderTests.kt | 48 ++++++++++-- ...KotlinSerializationProtobufDecoderTests.kt | 20 +++-- ...KotlinSerializationProtobufEncoderTests.kt | 13 +++- ...ializationCborHttpMessageConverterTests.kt | 28 ++++--- ...ializationJsonHttpMessageConverterTests.kt | 77 ++++++++++++++++--- ...zationProtobufHttpMessageConverterTests.kt | 28 ++++--- 26 files changed, 327 insertions(+), 159 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationBinaryDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationBinaryDecoder.java index 702f1cc3cc1..08209c8c86d 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationBinaryDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationBinaryDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -37,6 +37,10 @@ import org.springframework.util.MimeType; * Abstract base class for {@link Decoder} implementations that defer to Kotlin * {@linkplain BinaryFormat binary serializers}. * + *

As of Spring Framework 7.0, + * open polymorphism + * is supported. + * * @author Sebastien Deleuze * @author Iain Henderson * @author Arjen Poutsma diff --git a/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationBinaryEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationBinaryEncoder.java index d65a9e76f3b..46213448797 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationBinaryEncoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationBinaryEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -38,6 +38,10 @@ import org.springframework.util.MimeType; * Abstract base class for {@link Encoder} implementations that defer to Kotlin * {@linkplain BinaryFormat binary serializers}. * + *

As of Spring Framework 7.0, + * open polymorphism + * is supported. + * * @author Sebastien Deleuze * @author Iain Henderson * @author Arjen Poutsma 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 89ab88630bf..44339de4ab9 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -38,6 +38,10 @@ import org.springframework.util.MimeType; * Abstract base class for {@link Decoder} implementations that defer to Kotlin * {@linkplain StringFormat string serializers}. * + *

As of Spring Framework 7.0, + * open polymorphism + * is supported. + * * @author Sebastien Deleuze * @author Iain Henderson * @author Arjen Poutsma diff --git a/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationStringEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationStringEncoder.java index 000ed1eab4d..cb276221034 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationStringEncoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationStringEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -43,6 +43,10 @@ import org.springframework.util.MimeType; * Abstract base class for {@link Encoder} implementations that defer to Kotlin * {@linkplain StringFormat string serializers}. * + *

As of Spring Framework 7.0, + * open polymorphism + * is supported. + * * @author Sebastien Deleuze * @author Iain Henderson * @author Arjen Poutsma 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 7c8d57d1159..5bbbe6eb209 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 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. @@ -19,10 +19,8 @@ package org.springframework.http.codec; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.Arrays; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import kotlin.reflect.KFunction; import kotlin.reflect.KType; @@ -31,8 +29,6 @@ import kotlin.reflect.jvm.ReflectJvmMapping; import kotlinx.serialization.KSerializer; import kotlinx.serialization.SerialFormat; import kotlinx.serialization.SerializersKt; -import kotlinx.serialization.descriptors.PolymorphicKind; -import kotlinx.serialization.descriptors.SerialDescriptor; import org.jspecify.annotations.Nullable; import org.springframework.core.KotlinDetector; @@ -46,6 +42,10 @@ import org.springframework.util.MimeType; * Base class providing support methods for encoding and decoding with Kotlin * serialization. * + *

As of Spring Framework 7.0, + * open polymorphism + * is supported. + * * @author Sebastien Deleuze * @author Iain Henderson * @author Arjen Poutsma @@ -143,9 +143,6 @@ public abstract class KotlinSerializationSupport { catch (IllegalArgumentException ignored) { } if (serializer != null) { - if (hasPolymorphism(serializer.getDescriptor(), new HashSet<>())) { - return null; - } this.kTypeSerializerCache.put(type, serializer); } } @@ -162,27 +159,9 @@ public abstract class KotlinSerializationSupport { catch (IllegalArgumentException ignored) { } if (serializer != null) { - if (hasPolymorphism(serializer.getDescriptor(), new HashSet<>())) { - return null; - } this.typeSerializerCache.put(type, serializer); } } return serializer; } - - private static boolean hasPolymorphism(SerialDescriptor descriptor, Set alreadyProcessed) { - alreadyProcessed.add(descriptor.getSerialName()); - if (descriptor.getKind().equals(PolymorphicKind.OPEN.INSTANCE)) { - return true; - } - for (int i = 0 ; i < descriptor.getElementsCount() ; i++) { - SerialDescriptor elementDescriptor = descriptor.getElementDescriptor(i); - if (!alreadyProcessed.contains(elementDescriptor.getSerialName()) && hasPolymorphism(elementDescriptor, alreadyProcessed)) { - return true; - } - } - return false; - } - } diff --git a/spring-web/src/main/java/org/springframework/http/codec/cbor/KotlinSerializationCborDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/cbor/KotlinSerializationCborDecoder.java index 4e6f17d5b7e..2f3577eda2b 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/cbor/KotlinSerializationCborDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/cbor/KotlinSerializationCborDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -24,12 +24,12 @@ import org.springframework.http.codec.KotlinSerializationBinaryDecoder; /** * Decode a byte stream into CBOR and convert to Objects with * kotlinx.serialization. - * - *

This decoder can be used to bind {@code @Serializable} Kotlin classes, - * open polymorphic serialization - * is not supported. * It supports {@code application/cbor}. * + *

As of Spring Framework 7.0, + * open polymorphism + * is supported. + * *

Decoding streams is not supported yet, see * kotlinx.serialization/issues/1073 * related issue. diff --git a/spring-web/src/main/java/org/springframework/http/codec/cbor/KotlinSerializationCborEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/cbor/KotlinSerializationCborEncoder.java index af20d5bb708..6ce08dc2d76 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/cbor/KotlinSerializationCborEncoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/cbor/KotlinSerializationCborEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -24,12 +24,12 @@ import org.springframework.http.codec.KotlinSerializationBinaryEncoder; /** * Encode from an {@code Object} stream to a byte stream of CBOR objects using * kotlinx.serialization. - * - *

This encoder can be used to bind {@code @Serializable} Kotlin classes, - * open polymorphic serialization - * is not supported. * It supports {@code application/cbor}. * + *

As of Spring Framework 7.0, + * open polymorphism + * is supported. + * * @author Iain Henderson * @since 6.0 */ 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 2e52df2ccb8..f43bd5d833b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -24,13 +24,13 @@ import org.springframework.http.codec.KotlinSerializationStringDecoder; /** * Decode a byte stream into JSON and convert to Object's with * kotlinx.serialization. - * - *

This decoder can be used to bind {@code @Serializable} Kotlin classes, - * open polymorphic serialization - * is not supported. * It supports {@code application/json} and {@code application/*+json} with * various character sets, {@code UTF-8} being the default. * + *

As of Spring Framework 7.0, + * open polymorphism + * is supported. + * *

Decoding streams is not supported yet, see * kotlinx.serialization/issues/1073 * related issue. diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonEncoder.java index 77b2bef777e..229131518fb 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonEncoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -35,13 +35,13 @@ import org.springframework.util.MimeType; /** * Encode from an {@code Object} stream to a byte stream of JSON objects using * kotlinx.serialization. - * - *

This encoder can be used to bind {@code @Serializable} Kotlin classes, - * open polymorphic serialization - * is not supported. * It supports {@code application/json}, {@code application/x-ndjson} and {@code application/*+json} with * various character sets, {@code UTF-8} being the default. * + *

As of Spring Framework 7.0, + * open polymorphism + * is supported. + * * @author Sebastien Deleuze * @author Iain Henderson * @since 5.3 diff --git a/spring-web/src/main/java/org/springframework/http/codec/protobuf/KotlinSerializationProtobufDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/protobuf/KotlinSerializationProtobufDecoder.java index a8ebef55a42..80b2d221750 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/protobuf/KotlinSerializationProtobufDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/protobuf/KotlinSerializationProtobufDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -23,12 +23,12 @@ import org.springframework.http.codec.KotlinSerializationBinaryDecoder; /** * Decode a byte stream into a protocol Buffer and convert to Objects with * kotlinx.serialization. - * - *

This decoder can be used to bind {@code @Serializable} Kotlin classes, - * open polymorphic serialization - * is not supported. * It supports {@code application/x-protobuf}, {@code application/octet-stream}, and {@code application/vnd.google.protobuf}. * + *

As of Spring Framework 7.0, + * open polymorphism + * is supported. + * *

Decoding streams is not supported yet, see * kotlinx.serialization/issues/1073 * related issue. diff --git a/spring-web/src/main/java/org/springframework/http/codec/protobuf/KotlinSerializationProtobufEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/protobuf/KotlinSerializationProtobufEncoder.java index d9b0fc0b291..c99cfa97c3d 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/protobuf/KotlinSerializationProtobufEncoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/protobuf/KotlinSerializationProtobufEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -23,12 +23,12 @@ import org.springframework.http.codec.KotlinSerializationBinaryEncoder; /** * Decode a byte stream into a Protocol Buffer and convert to Objects with * kotlinx.serialization. - * - *

This decoder can be used to bind {@code @Serializable} Kotlin classes, - * open polymorphic serialization - * is not supported. * It supports {@code application/x-protobuf}, {@code application/octet-stream}, and {@code application/vnd.google.protobuf}. * + *

As of Spring Framework 7.0, + * open polymorphism + * is supported. + * *

Decoding streams is not supported yet, see * kotlinx.serialization/issues/1073 * related issue. 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 ca0c65695a3..73a6a35a84b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -19,9 +19,7 @@ package org.springframework.http.converter; import java.io.IOException; import java.lang.reflect.Method; import java.lang.reflect.Type; -import java.util.HashSet; import java.util.Map; -import java.util.Set; import kotlin.reflect.KFunction; import kotlin.reflect.KType; @@ -30,8 +28,6 @@ import kotlin.reflect.jvm.ReflectJvmMapping; import kotlinx.serialization.KSerializer; import kotlinx.serialization.SerialFormat; import kotlinx.serialization.SerializersKt; -import kotlinx.serialization.descriptors.PolymorphicKind; -import kotlinx.serialization.descriptors.SerialDescriptor; import org.jspecify.annotations.Nullable; import org.springframework.core.KotlinDetector; @@ -48,6 +44,10 @@ import org.springframework.util.ConcurrentReferenceHashMap; * Abstract base class for {@link HttpMessageConverter} implementations that * use Kotlin serialization. * + *

As of Spring Framework 7.0, + * open polymorphism + * is supported. + * * @author Andreas Ahlenstorf * @author Sebastien Deleuze * @author Juergen Hoeller @@ -160,9 +160,6 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter())) { - return null; - } this.kTypeSerializerCache.put(type, serializer); } } @@ -179,29 +176,12 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter())) { - return null; - } this.typeSerializerCache.put(type, serializer); } } return serializer; } - private boolean hasPolymorphism(SerialDescriptor descriptor, Set alreadyProcessed) { - alreadyProcessed.add(descriptor.getSerialName()); - if (descriptor.getKind().equals(PolymorphicKind.OPEN.INSTANCE)) { - return true; - } - for (int i = 0 ; i < descriptor.getElementsCount() ; i++) { - SerialDescriptor elementDescriptor = descriptor.getElementDescriptor(i); - if (!alreadyProcessed.contains(elementDescriptor.getSerialName()) && hasPolymorphism(elementDescriptor, alreadyProcessed)) { - return true; - } - } - return false; - } - @Override protected boolean supportsRepeatableWrites(Object object) { return true; diff --git a/spring-web/src/main/java/org/springframework/http/converter/KotlinSerializationBinaryHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/KotlinSerializationBinaryHttpMessageConverter.java index bc094e9fe4b..8fde78bfe99 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/KotlinSerializationBinaryHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/KotlinSerializationBinaryHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -31,6 +31,10 @@ import org.springframework.util.StreamUtils; * Abstract base class for {@link HttpMessageConverter} implementations that * defer to Kotlin {@linkplain BinaryFormat binary serializers}. * + *

As of Spring Framework 7.0, + * open polymorphism + * is supported. + * * @author Andreas Ahlenstorf * @author Sebastien Deleuze * @author Juergen Hoeller diff --git a/spring-web/src/main/java/org/springframework/http/converter/KotlinSerializationStringHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/KotlinSerializationStringHttpMessageConverter.java index 9b803ec68a9..9a33ddd3563 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/KotlinSerializationStringHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/KotlinSerializationStringHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -34,6 +34,10 @@ import org.springframework.util.StreamUtils; * Abstract base class for {@link HttpMessageConverter} implementations that * defer to Kotlin {@linkplain StringFormat string serializers}. * + *

As of Spring Framework 7.0, + * open polymorphism + * is supported. + * * @author Andreas Ahlenstorf * @author Sebastien Deleuze * @author Juergen Hoeller diff --git a/spring-web/src/main/java/org/springframework/http/converter/cbor/KotlinSerializationCborHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/cbor/KotlinSerializationCborHttpMessageConverter.java index 544070128bf..2918dd9d65c 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/cbor/KotlinSerializationCborHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/cbor/KotlinSerializationCborHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -25,12 +25,12 @@ import org.springframework.http.converter.KotlinSerializationBinaryHttpMessageCo * Implementation of {@link org.springframework.http.converter.HttpMessageConverter} * that can read and write CBOR using * kotlinx.serialization. - * - *

This converter can be used to bind {@code @Serializable} Kotlin classes, - * open polymorphic serialization - * is not supported. * It supports {@code application/cbor}. * + *

As of Spring Framework 7.0, + * open polymorphism + * is supported. + * * @author Iain Henderson * @since 6.0 */ diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java index bb47542d607..458a3d95c1c 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -25,13 +25,13 @@ import org.springframework.http.converter.KotlinSerializationStringHttpMessageCo * Implementation of {@link org.springframework.http.converter.HttpMessageConverter} * that can read and write JSON using * kotlinx.serialization. - * - *

This converter can be used to bind {@code @Serializable} Kotlin classes, - * open polymorphic serialization - * is not supported. * It supports {@code application/json} and {@code application/*+json} with * various character sets, {@code UTF-8} being the default. * + *

As of Spring Framework 7.0, + * open polymorphism + * is supported. + * * @author Andreas Ahlenstorf * @author Sebastien Deleuze * @author Juergen Hoeller diff --git a/spring-web/src/main/java/org/springframework/http/converter/protobuf/KotlinSerializationProtobufHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/protobuf/KotlinSerializationProtobufHttpMessageConverter.java index 44448fb774c..691ff2c1703 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/protobuf/KotlinSerializationProtobufHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/protobuf/KotlinSerializationProtobufHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -25,12 +25,12 @@ import org.springframework.http.converter.KotlinSerializationBinaryHttpMessageCo * Implementation of {@link org.springframework.http.converter.HttpMessageConverter} * that can read and write Protocol Buffers using * kotlinx.serialization. - * - *

This converter can be used to bind {@code @Serializable} Kotlin classes, - * open polymorphic serialization - * is not supported. * It supports {@code application/x-protobuf}, {@code application/octet-stream}, and {@code application/vnd.google.protobuf}. * + *

As of Spring Framework 7.0, + * open polymorphism + * is supported. + * * @author Iain Henderson * @since 6.0 */ diff --git a/spring-web/src/test/kotlin/org/springframework/http/codec/cbor/KotlinSerializationCborDecoderTests.kt b/spring-web/src/test/kotlin/org/springframework/http/codec/cbor/KotlinSerializationCborDecoderTests.kt index bfba56dd4b3..0c5453c004c 100644 --- a/spring-web/src/test/kotlin/org/springframework/http/codec/cbor/KotlinSerializationCborDecoderTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/http/codec/cbor/KotlinSerializationCborDecoderTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 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. @@ -42,7 +42,6 @@ import org.springframework.http.MediaType @ExperimentalSerializationApi class KotlinSerializationCborDecoderTests : AbstractDecoderTests(KotlinSerializationCborDecoder()) { - @Suppress("UsePropertyAccessSyntax", "DEPRECATION") @Test override fun canDecode() { assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java), MediaType.APPLICATION_CBOR)).isTrue() @@ -51,11 +50,13 @@ class KotlinSerializationCborDecoderTests : AbstractDecoderTests = customDecoder.decode(input, resolvableType, null, null) as Flux + val step: FirstStep = StepVerifier.create(result) + + step.expectNext(SimpleSerializableBean("foo")) + .expectNext(SimpleSerializableBean("bar")) + .expectNext(SimpleSerializableBean("baz")) + .verifyComplete() + } + @Test fun decodeWithUnexpectedFormat() { val input = Flux.concat( @@ -188,4 +217,17 @@ class KotlinSerializationJsonDecoderTests : AbstractDecoderTests) = map + interface ISimpleSerializableBean { + val name: String + } + + @Serializable + data class SimpleSerializableBean(override val name: String): ISimpleSerializableBean + + class OrderedImpl : Ordered { + override fun getOrder(): Int { + return 0 + } + } + } 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 d5c92147929..0edd8d5b5e3 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -17,11 +17,14 @@ package org.springframework.http.codec.json import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.modules.SerializersModule import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.springframework.core.MethodParameter import org.springframework.core.Ordered import org.springframework.core.ResolvableType +import org.springframework.core.io.buffer.DataBuffer import org.springframework.core.testfixture.codec.AbstractEncoderTests import org.springframework.http.MediaType import org.springframework.http.codec.ServerSentEvent @@ -53,11 +56,9 @@ class KotlinSerializationJsonEncoderTests : AbstractEncoderTests.just( + SimpleSerializableBean("foo"), + SimpleSerializableBean("bar"), + SimpleSerializableBean("baz") + ) + val result = customEncoder.encode(input, this.bufferFactory, ResolvableType.forClass(ISimpleSerializableBean::class.java), MediaType.APPLICATION_NDJSON, null) + val step = StepVerifier.create(result) + step + .consumeNextWith { expectString("{\"type\":\"org.springframework.http.codec.json.KotlinSerializationJsonEncoderTests.SimpleSerializableBean\",\"name\":\"foo\"}\n").accept(it) } + .consumeNextWith { expectString("{\"type\":\"org.springframework.http.codec.json.KotlinSerializationJsonEncoderTests.SimpleSerializableBean\",\"name\":\"bar\"}\n").accept(it) } + .consumeNextWith { expectString("{\"type\":\"org.springframework.http.codec.json.KotlinSerializationJsonEncoderTests.SimpleSerializableBean\",\"name\":\"baz\"}\n").accept(it) } + .verifyComplete() + } + @Test fun encodeMono() { val input = Mono.just(Pojo("foo", "bar")) @@ -141,8 +164,10 @@ class KotlinSerializationJsonEncoderTests : AbstractEncoderTests(KotlinSerializationProtobufEncoder()) { @@ -59,7 +58,7 @@ class KotlinSerializationProtobufEncoderTests : AbstractEncoderTests>(), MediaType.APPLICATION_CBOR)).isTrue() - assertThat(converter.canRead(List::class.java, MediaType.APPLICATION_CBOR)).isFalse() + assertThat(converter.canRead(List::class.java, MediaType.APPLICATION_CBOR)).isTrue() assertThat(converter.canRead(resolvableTypeOf>(), MediaType.APPLICATION_CBOR)).isTrue() - assertThat(converter.canRead(Set::class.java, MediaType.APPLICATION_CBOR)).isFalse() + assertThat(converter.canRead(Set::class.java, MediaType.APPLICATION_CBOR)).isTrue() assertThat(converter.canRead(resolvableTypeOf>(), MediaType.APPLICATION_CBOR)).isTrue() assertThat(converter.canRead(resolvableTypeOf>(), MediaType.APPLICATION_CBOR)).isTrue() assertThat(converter.canRead(resolvableTypeOf>(), MediaType.APPLICATION_CBOR)).isTrue() assertThat(converter.canRead(resolvableTypeOf>(), MediaType.APPLICATION_JSON)).isFalse() - assertThat(converter.canRead(resolvableTypeOf(), MediaType.APPLICATION_CBOR)).isFalse() - assertThat(converter.canRead(resolvableTypeOf>(), MediaType.APPLICATION_CBOR)).isFalse() + assertThat(converter.canRead(resolvableTypeOf(), MediaType.APPLICATION_CBOR)).isTrue() + assertThat(converter.canRead(resolvableTypeOf>(), MediaType.APPLICATION_CBOR)).isTrue() + assertThat(converter.canRead(resolvableTypeOf(), MediaType.APPLICATION_CBOR)).isFalse() + assertThat(converter.canRead(resolvableTypeOf>(), MediaType.APPLICATION_CBOR)).isFalse() } @Test @@ -88,18 +89,19 @@ class KotlinSerializationCborHttpMessageConverterTests { assertThat(converter.canWrite(String::class.java, MediaType.APPLICATION_CBOR)).isTrue() assertThat(converter.canWrite(NotSerializableBean::class.java, MediaType.APPLICATION_CBOR)).isFalse() - assertThat(converter.canWrite(Map::class.java, MediaType.APPLICATION_CBOR)).isFalse() + assertThat(converter.canWrite(Map::class.java, MediaType.APPLICATION_CBOR)).isTrue() assertThat(converter.canWrite(resolvableTypeOf>(), Map::class.java, MediaType.APPLICATION_CBOR)).isTrue() - assertThat(converter.canWrite(List::class.java, MediaType.APPLICATION_CBOR)).isFalse() + assertThat(converter.canWrite(List::class.java, MediaType.APPLICATION_CBOR)).isTrue() assertThat(converter.canWrite(resolvableTypeOf>(), List::class.java, MediaType.APPLICATION_CBOR)).isTrue() - assertThat(converter.canWrite(Set::class.java, MediaType.APPLICATION_CBOR)).isFalse() + assertThat(converter.canWrite(Set::class.java, MediaType.APPLICATION_CBOR)).isTrue() assertThat(converter.canWrite(resolvableTypeOf>(), Set::class.java, MediaType.APPLICATION_CBOR)).isTrue() assertThat(converter.canWrite(resolvableTypeOf>(), List::class.java, MediaType.APPLICATION_CBOR)).isTrue() assertThat(converter.canWrite(resolvableTypeOf>(), List::class.java, MediaType.APPLICATION_CBOR)).isTrue() assertThat(converter.canWrite(resolvableTypeOf>(), List::class.java, MediaType.APPLICATION_JSON)).isFalse() - assertThat(converter.canWrite(resolvableTypeOf(), Ordered::class.java, MediaType.APPLICATION_CBOR)).isFalse() + assertThat(converter.canWrite(resolvableTypeOf(), Ordered::class.java, MediaType.APPLICATION_CBOR)).isTrue() + assertThat(converter.canWrite(resolvableTypeOf(), Ordered::class.java, MediaType.APPLICATION_CBOR)).isFalse() } @Test @@ -228,4 +230,10 @@ class KotlinSerializationCborHttpMessageConverterTests { return ResolvableType.forType((superType as ParameterizedType).actualTypeArguments.first()!!) } + class OrderedImpl : Ordered { + override fun getOrder(): Int { + return 0 + } + } + } 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 74cd1ce9bd3..0b9b8f01847 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 @@ -17,6 +17,8 @@ package org.springframework.http.converter.json import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.modules.SerializersModule import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.jupiter.api.Test @@ -53,19 +55,21 @@ class KotlinSerializationJsonHttpMessageConverterTests { assertThat(converter.canRead(String::class.java, MediaType.APPLICATION_JSON)).isTrue() assertThat(converter.canRead(NotSerializableBean::class.java, MediaType.APPLICATION_JSON)).isFalse() - assertThat(converter.canRead(Map::class.java, MediaType.APPLICATION_JSON)).isFalse() + assertThat(converter.canRead(Map::class.java, MediaType.APPLICATION_JSON)).isTrue() assertThat(converter.canRead(resolvableTypeOf>(), MediaType.APPLICATION_JSON)).isTrue() - assertThat(converter.canRead(List::class.java, MediaType.APPLICATION_JSON)).isFalse() + assertThat(converter.canRead(List::class.java, MediaType.APPLICATION_JSON)).isTrue() assertThat(converter.canRead(resolvableTypeOf>(), MediaType.APPLICATION_JSON)).isTrue() - assertThat(converter.canRead(Set::class.java, MediaType.APPLICATION_JSON)).isFalse() + assertThat(converter.canRead(Set::class.java, MediaType.APPLICATION_JSON)).isTrue() assertThat(converter.canRead(resolvableTypeOf>(), MediaType.APPLICATION_JSON)).isTrue() assertThat(converter.canRead(resolvableTypeOf>(), MediaType.APPLICATION_JSON)).isTrue() assertThat(converter.canRead(resolvableTypeOf>(), MediaType.APPLICATION_JSON)).isTrue() assertThat(converter.canRead(resolvableTypeOf>(), MediaType.APPLICATION_PDF)).isFalse() - assertThat(converter.canRead(resolvableTypeOf(), MediaType.APPLICATION_JSON)).isFalse() - assertThat(converter.canRead(resolvableTypeOf>(), MediaType.APPLICATION_JSON)).isFalse() + assertThat(converter.canRead(resolvableTypeOf(), MediaType.APPLICATION_JSON)).isTrue() + assertThat(converter.canRead(resolvableTypeOf>(), MediaType.APPLICATION_JSON)).isTrue() + assertThat(converter.canRead(resolvableTypeOf(), MediaType.APPLICATION_JSON)).isFalse() + assertThat(converter.canRead(resolvableTypeOf>(), MediaType.APPLICATION_JSON)).isFalse() assertThat(converter.canRead(ResolvableType.forType(ResolvableType.NONE.type), MediaType.APPLICATION_JSON)).isFalse() @@ -79,18 +83,19 @@ class KotlinSerializationJsonHttpMessageConverterTests { assertThat(converter.canWrite(String::class.java, MediaType.APPLICATION_JSON)).isTrue() assertThat(converter.canWrite(NotSerializableBean::class.java, MediaType.APPLICATION_JSON)).isFalse() - assertThat(converter.canWrite(Map::class.java, MediaType.APPLICATION_JSON)).isFalse() + assertThat(converter.canWrite(Map::class.java, MediaType.APPLICATION_JSON)).isTrue() assertThat(converter.canWrite(resolvableTypeOf>(), Map::class.java, MediaType.APPLICATION_JSON)).isTrue() - assertThat(converter.canWrite(List::class.java, MediaType.APPLICATION_JSON)).isFalse() + assertThat(converter.canWrite(List::class.java, MediaType.APPLICATION_JSON)).isTrue() assertThat(converter.canWrite(resolvableTypeOf>(), List::class.java, MediaType.APPLICATION_JSON)).isTrue() - assertThat(converter.canWrite(Set::class.java, MediaType.APPLICATION_JSON)).isFalse() + assertThat(converter.canWrite(Set::class.java, MediaType.APPLICATION_JSON)).isTrue() assertThat(converter.canWrite(resolvableTypeOf>(), Set::class.java, MediaType.APPLICATION_JSON)).isTrue() assertThat(converter.canWrite(resolvableTypeOf>(), List::class.java, MediaType.APPLICATION_JSON)).isTrue() assertThat(converter.canWrite(resolvableTypeOf>(), List::class.java, MediaType.APPLICATION_JSON)).isTrue() assertThat(converter.canWrite(resolvableTypeOf>(), List::class.java, MediaType.APPLICATION_PDF)).isFalse() - assertThat(converter.canWrite(resolvableTypeOf(), Ordered::class.java, MediaType.APPLICATION_JSON)).isFalse() + assertThat(converter.canWrite(resolvableTypeOf(), Ordered::class.java, MediaType.APPLICATION_JSON)).isTrue() + assertThat(converter.canWrite(resolvableTypeOf(), OrderedImpl::class.java, MediaType.APPLICATION_JSON)).isFalse() assertThat(converter.canWrite(ResolvableType.NONE, SerializableBean::class.java, MediaType.APPLICATION_JSON)).isFalse() @@ -246,6 +251,26 @@ class KotlinSerializationJsonHttpMessageConverterTests { assertThat(result).containsExactlyEntriesOf(mapOf("value" to null)) } + @Test + @Suppress("UNCHECKED_CAST") + fun readWithPolymorphism() { + val json = Json { + serializersModule = SerializersModule { + polymorphic(ISimpleSerializableBean::class, SimpleSerializableBean::class, SimpleSerializableBean.serializer()) + } + } + val customConverter = KotlinSerializationJsonHttpMessageConverter(json) + val body = """[{"type":"org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverterTests.SimpleSerializableBean","name":"foo"},""" + + """{"type":"org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverterTests.SimpleSerializableBean","name":"bar"}]""" + + val inputMessage = MockHttpInputMessage(body.toByteArray(StandardCharsets.UTF_8)) + inputMessage.headers.contentType = MediaType.APPLICATION_JSON + val resolvableType = ResolvableType.forClassWithGenerics(List::class.java, ISimpleSerializableBean::class.java) + val result = customConverter.read(resolvableType, inputMessage, null) as List + + assertThat(result).containsExactly(SimpleSerializableBean("foo"), SimpleSerializableBean("bar")) + } + @Test fun writeObject() { val outputMessage = MockHttpOutputMessage() @@ -398,6 +423,27 @@ class KotlinSerializationJsonHttpMessageConverterTests { assertThat(result).isEqualTo("42") } + @Test + fun writeWithPolymorphism() { + val json = Json { + serializersModule = SerializersModule { + polymorphic(ISimpleSerializableBean::class, SimpleSerializableBean::class, SimpleSerializableBean.serializer()) + } + } + val customConverter = KotlinSerializationJsonHttpMessageConverter(json) + + val outputMessage = MockHttpOutputMessage() + val value = listOf(SimpleSerializableBean("foo"), SimpleSerializableBean("bar")) + customConverter.write(value, ResolvableType.forClassWithGenerics(List::class.java, ISimpleSerializableBean::class.java), null, outputMessage, null) + + val result = outputMessage.getBodyAsString(StandardCharsets.UTF_8) + + assertThat(outputMessage.headers.containsHeaderValue("Content-Type", "application/json")).isTrue() + assertThat(result).isEqualTo( + """[{"type":"org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverterTests.SimpleSerializableBean","name":"foo"},""" + + """{"type":"org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverterTests.SimpleSerializableBean","name":"bar"}]""") + } + @Serializable @Suppress("ArrayInDataClass") @@ -426,4 +472,17 @@ class KotlinSerializationJsonHttpMessageConverterTests { val value: Int get() = 42 + interface ISimpleSerializableBean { + val name: String + } + + @Serializable + data class SimpleSerializableBean(override val name: String): ISimpleSerializableBean + + class OrderedImpl : Ordered { + override fun getOrder(): Int { + return 0 + } + } + } diff --git a/spring-web/src/test/kotlin/org/springframework/http/converter/protobuf/KotlinSerializationProtobufHttpMessageConverterTests.kt b/spring-web/src/test/kotlin/org/springframework/http/converter/protobuf/KotlinSerializationProtobufHttpMessageConverterTests.kt index 36ebe4be7eb..613045c9b1e 100644 --- a/spring-web/src/test/kotlin/org/springframework/http/converter/protobuf/KotlinSerializationProtobufHttpMessageConverterTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/http/converter/protobuf/KotlinSerializationProtobufHttpMessageConverterTests.kt @@ -41,7 +41,6 @@ import kotlin.reflect.typeOf * @author Sebastien Deleuze * @author Iain Henderson */ -@Suppress("UsePropertyAccessSyntax") @ExperimentalSerializationApi class KotlinSerializationProtobufHttpMessageConverterTests { @@ -68,18 +67,20 @@ class KotlinSerializationProtobufHttpMessageConverterTests { assertThat(converter.canRead(String::class.java, mimeType)).isTrue() assertThat(converter.canRead(NotSerializableBean::class.java, mimeType)).isFalse() - assertThat(converter.canRead(Map::class.java, mimeType)).isFalse() + assertThat(converter.canRead(Map::class.java, mimeType)).isTrue() assertThat(converter.canRead(resolvableTypeOf>(), mimeType)).isTrue() - assertThat(converter.canRead(List::class.java, mimeType)).isFalse() + assertThat(converter.canRead(List::class.java, mimeType)).isTrue() assertThat(converter.canRead(resolvableTypeOf>(), mimeType)).isTrue() - assertThat(converter.canRead(Set::class.java, mimeType)).isFalse() + assertThat(converter.canRead(Set::class.java, mimeType)).isTrue() assertThat(converter.canRead(resolvableTypeOf>(), mimeType)).isTrue() assertThat(converter.canRead(resolvableTypeOf>(), mimeType)).isTrue() assertThat(converter.canRead(resolvableTypeOf>(),mimeType)).isTrue() - assertThat(converter.canRead(resolvableTypeOf(), mimeType)).isFalse() - assertThat(converter.canRead(resolvableTypeOf>(), mimeType)).isFalse() + assertThat(converter.canRead(resolvableTypeOf(), mimeType)).isTrue() + assertThat(converter.canRead(resolvableTypeOf>(), mimeType)).isTrue() + assertThat(converter.canRead(resolvableTypeOf(), mimeType)).isFalse() + assertThat(converter.canRead(resolvableTypeOf>(), mimeType)).isFalse() } assertThat(converter.canRead(SerializableBean::class.java, MediaType.APPLICATION_JSON)).isFalse() assertThat(converter.canRead(resolvableTypeOf>(), MediaType.APPLICATION_JSON)).isFalse() @@ -92,17 +93,18 @@ class KotlinSerializationProtobufHttpMessageConverterTests { assertThat(converter.canWrite(String::class.java, mimeType)).isTrue() assertThat(converter.canWrite(NotSerializableBean::class.java, mimeType)).isFalse() - assertThat(converter.canWrite(Map::class.java, mimeType)).isFalse() + assertThat(converter.canWrite(Map::class.java, mimeType)).isTrue() assertThat(converter.canWrite(resolvableTypeOf>(), Map::class.java, mimeType)).isTrue() - assertThat(converter.canWrite(List::class.java, mimeType)).isFalse() + assertThat(converter.canWrite(List::class.java, mimeType)).isTrue() assertThat(converter.canWrite(resolvableTypeOf>(), List::class.java, mimeType)).isTrue() - assertThat(converter.canWrite(Set::class.java, mimeType)).isFalse() + assertThat(converter.canWrite(Set::class.java, mimeType)).isTrue() assertThat(converter.canWrite(resolvableTypeOf>(), Set::class.java, mimeType)).isTrue() assertThat(converter.canWrite(resolvableTypeOf>(), List::class.java, mimeType)).isTrue() assertThat(converter.canWrite(resolvableTypeOf>(), List::class.java, mimeType)).isTrue() - assertThat(converter.canWrite(resolvableTypeOf(), Ordered::class.java, mimeType)).isFalse() + assertThat(converter.canWrite(resolvableTypeOf(), Ordered::class.java, mimeType)).isTrue() + assertThat(converter.canWrite(resolvableTypeOf(), OrderedImpl::class.java, mimeType)).isTrue() } assertThat(converter.canWrite(SerializableBean::class.java, MediaType.APPLICATION_JSON)).isFalse() @@ -243,4 +245,10 @@ class KotlinSerializationProtobufHttpMessageConverterTests { return ResolvableType.forType((superType as ParameterizedType).actualTypeArguments.first()!!) } + class OrderedImpl : Ordered { + override fun getOrder(): Int { + return 0 + } + } + }