From 19dd488dd7fd6fecec6acb1f28210ed0572237e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 7 Nov 2025 16:31:29 +0100 Subject: [PATCH] Prevent Kotlin Serialization codecs side effects This commit updates Kotlin serialization codecs to perform an additional check invoking KotlinDetector#hasSerializableAnnotation to decide if the related type should be processed or not. The goal is to prevent in the default arrangement conflicts between general purpose codecs like Jackson and Kotlin serialization when both are used. New constructors allowing to specify a custom predicate are also introduced. See gh-35761 --- .../KotlinSerializationBinaryDecoder.java | 25 +++++++- .../KotlinSerializationBinaryEncoder.java | 25 +++++++- .../KotlinSerializationStringDecoder.java | 25 +++++++- .../KotlinSerializationStringEncoder.java | 26 ++++++++- .../codec/KotlinSerializationSupport.java | 39 +++++++++---- .../cbor/KotlinSerializationCborDecoder.java | 47 ++++++++++++++- .../cbor/KotlinSerializationCborEncoder.java | 46 ++++++++++++++- .../json/KotlinSerializationJsonDecoder.java | 55 ++++++++++++++++-- .../json/KotlinSerializationJsonEncoder.java | 52 +++++++++++++++-- .../KotlinSerializationProtobufDecoder.java | 46 ++++++++++++++- .../KotlinSerializationProtobufEncoder.java | 45 +++++++++++++- .../KotlinSerializationCborDecoderTests.kt | 29 ++++++++-- .../KotlinSerializationCborEncoderTests.kt | 22 +++++-- ...stomKotlinSerializationJsonDecoderTests.kt | 2 +- ...stomKotlinSerializationJsonEncoderTests.kt | 2 +- .../KotlinSerializationJsonDecoderTests.kt | 38 ++++++++++-- .../KotlinSerializationJsonEncoderTests.kt | 58 ++++++++++++++----- ...KotlinSerializationProtobufDecoderTests.kt | 6 +- ...KotlinSerializationProtobufEncoderTests.kt | 30 ++++++++-- 19 files changed, 533 insertions(+), 85 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 46d5e9f75b4..c123629434b 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 @@ -18,6 +18,7 @@ package org.springframework.http.codec; import java.util.List; import java.util.Map; +import java.util.function.Predicate; import kotlinx.serialization.BinaryFormat; import kotlinx.serialization.KSerializer; @@ -37,9 +38,11 @@ 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. + *

As of Spring Framework 7.0, by default it only decodes types annotated with + * {@link kotlinx.serialization.Serializable @Serializable} at type or generics level + * since it allows combined usage with other general purpose decoders without conflicts. + * Alternative constructors with a {@code Predicate} parameter can be used + * to customize this behavior. * * @author Sebastien Deleuze * @author Iain Henderson @@ -54,10 +57,26 @@ public abstract class KotlinSerializationBinaryDecoder e private final ByteArrayDecoder byteArrayDecoder = new ByteArrayDecoder(); + /** + * Creates a new instance with the given format and supported mime types + * which only decodes types annotated with + * {@link kotlinx.serialization.Serializable @Serializable} at type or + * generics level. + */ public KotlinSerializationBinaryDecoder(T format, MimeType... supportedMimeTypes) { super(format, supportedMimeTypes); } + /** + * Creates a new instance with the given format and supported mime types + * which only decodes types for which the specified predicate returns + * {@code true}. + * @since 7.0 + */ + public KotlinSerializationBinaryDecoder(T format, Predicate typePredicate, MimeType... supportedMimeTypes) { + super(format, typePredicate, supportedMimeTypes); + } + /** * Configure a limit on the number of bytes that can be buffered whenever * the input stream needs to be aggregated. This can be a result of 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 3a3170b0e98..3b5f18d979d 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 @@ -18,6 +18,7 @@ package org.springframework.http.codec; import java.util.List; import java.util.Map; +import java.util.function.Predicate; import kotlinx.serialization.BinaryFormat; import kotlinx.serialization.KSerializer; @@ -38,9 +39,11 @@ 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. + *

As of Spring Framework 7.0, by default it only encodes types annotated with + * {@link kotlinx.serialization.Serializable @Serializable} at type or generics level + * since it allows combined usage with other general purpose encoders without conflicts. + * Alternative constructors with a {@code Predicate} parameter can be used + * to customize this behavior. * * @author Sebastien Deleuze * @author Iain Henderson @@ -54,10 +57,26 @@ public abstract class KotlinSerializationBinaryEncoder e // ByteArraySequence encoding needed for now, see https://github.com/Kotlin/kotlinx.serialization/issues/204 for more details private final ByteArrayEncoder byteArrayEncoder = new ByteArrayEncoder(); + /** + * Creates a new instance with the given format and supported mime types + * which only encodes types annotated with + * {@link kotlinx.serialization.Serializable @Serializable} at type or + * generics level. + */ protected KotlinSerializationBinaryEncoder(T format, MimeType... supportedMimeTypes) { super(format, supportedMimeTypes); } + /** + * Creates a new instance with the given format and supported mime types + * which only encodes types for which the specified predicate returns + * {@code true}. + * @since 7.0 + */ + protected KotlinSerializationBinaryEncoder(T format, Predicate typePredicate, MimeType... supportedMimeTypes) { + super(format, typePredicate, supportedMimeTypes); + } + @Override public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) { return canSerialize(elementType, mimeType); 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 423aa4087cf..684df592298 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 @@ -18,6 +18,7 @@ package org.springframework.http.codec; import java.util.List; import java.util.Map; +import java.util.function.Predicate; import kotlinx.serialization.KSerializer; import kotlinx.serialization.StringFormat; @@ -38,9 +39,11 @@ 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. + *

As of Spring Framework 7.0, by default it only decodes types annotated with + * {@link kotlinx.serialization.Serializable @Serializable} at type or generics level + * since it allows combined usage with other general purpose decoders without conflicts. + * Alternative constructors with a {@code Predicate} parameter can be used + * to customize this behavior. * * @author Sebastien Deleuze * @author Iain Henderson @@ -55,10 +58,26 @@ public abstract class KotlinSerializationStringDecoder e private final StringDecoder stringDecoder = StringDecoder.allMimeTypes(StringDecoder.DEFAULT_DELIMITERS, false); + /** + * Creates a new instance with the given format and supported mime types + * which only decodes types annotated with + * {@link kotlinx.serialization.Serializable @Serializable} at type or + * generics level. + */ public KotlinSerializationStringDecoder(T format, MimeType... supportedMimeTypes) { super(format, supportedMimeTypes); } + /** + * Creates a new instance with the given format and supported mime types + * which only decodes types for which the specified predicate returns + * {@code true}. + * @since 7.0 + */ + public KotlinSerializationStringDecoder(T format, Predicate typePredicate, MimeType... supportedMimeTypes) { + super(format, typePredicate, supportedMimeTypes); + } + /** * Configure a limit on the number of bytes that can be buffered whenever * the input stream needs to be aggregated. This can be a result of 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 b150ada67cc..18d7330b453 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 @@ -22,6 +22,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Predicate; import kotlinx.serialization.KSerializer; import kotlinx.serialization.StringFormat; @@ -43,9 +44,11 @@ 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. + *

As of Spring Framework 7.0, by default it only encodes types annotated with + * {@link kotlinx.serialization.Serializable @Serializable} at type or generics level + * since it allows combined usage with other general purpose encoders without conflicts. + * Alternative constructors with a {@code Predicate} parameter can be used + * to customize this behavior. * * @author Sebastien Deleuze * @author Iain Henderson @@ -65,10 +68,27 @@ public abstract class KotlinSerializationStringEncoder e private final CharSequenceEncoder charSequenceEncoder = CharSequenceEncoder.allMimeTypes(); private final Set streamingMediaTypes = new HashSet<>(); + + /** + * Creates a new instance with the given format and supported mime types + * which only encodes types annotated with + * {@link kotlinx.serialization.Serializable @Serializable} at type or + * generics level. + */ protected KotlinSerializationStringEncoder(T format, MimeType... supportedMimeTypes) { super(format, supportedMimeTypes); } + /** + * Creates a new instance with the given format and supported mime types + * which only encodes types for which the specified predicate returns + * {@code true}. + * @since 7.0 + */ + protected KotlinSerializationStringEncoder(T format, Predicate typePredicate, MimeType... supportedMimeTypes) { + super(format, typePredicate, supportedMimeTypes); + } + /** * Set streaming {@link MediaType MediaTypes}. * @param streamingMediaTypes streaming {@link MediaType MediaTypes} 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 7744911da0e..1d144926c0a 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 @@ -21,6 +21,7 @@ import java.lang.reflect.Type; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.function.Predicate; import kotlin.reflect.KFunction; import kotlin.reflect.KType; @@ -42,9 +43,11 @@ 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. + *

As of Spring Framework 7.0, by default it only handles types annotated with + * {@link kotlinx.serialization.Serializable @Serializable} at type or generics level + * since it allows combined usage with other general purpose decoders without conflicts. + * Alternative constructors with a {@code Predicate} parameter can be used + * to customize this behavior. * * @author Sebastien Deleuze * @author Iain Henderson @@ -63,12 +66,29 @@ public abstract class KotlinSerializationSupport { private final List supportedMimeTypes; + private final Predicate typePredicate; + /** - * Creates a new instance of this support class with the given format - * and supported mime types. + * Creates a new instance with the given format and supported mime types + * which only handle types annotated with + * {@link kotlinx.serialization.Serializable @Serializable} at type or + * generics level. */ protected KotlinSerializationSupport(T format, MimeType... supportedMimeTypes) { this.format = format; + this.typePredicate = KotlinDetector::hasSerializableAnnotation; + this.supportedMimeTypes = Arrays.asList(supportedMimeTypes); + } + + /** + * Creates a new instance with the given format and supported mime types + * which only encode types for which the specified predicate returns + * {@code true}. + * @since 7.0 + */ + protected KotlinSerializationSupport(T format, Predicate typePredicate, MimeType... supportedMimeTypes) { + this.format = format; + this.typePredicate = typePredicate; this.supportedMimeTypes = Arrays.asList(supportedMimeTypes); } @@ -94,15 +114,10 @@ public abstract class KotlinSerializationSupport { * @return {@code true} if {@code type} can be serialized; false otherwise */ protected final boolean canSerialize(ResolvableType type, @Nullable MimeType mimeType) { - KSerializer serializer = serializer(type); - if (serializer == null) { + if (!this.typePredicate.test(type) || ResolvableType.NONE.equals(type)) { return false; } - else { - return (supports(mimeType) && !String.class.isAssignableFrom(type.toClass()) && - !ServerSentEvent.class.isAssignableFrom(type.toClass())); - } - + return serializer(type) != null && supports(mimeType); } private boolean supports(@Nullable MimeType mimeType) { 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 f89566e2eb7..15c29eeb80d 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 @@ -16,8 +16,11 @@ package org.springframework.http.codec.cbor; +import java.util.function.Predicate; + import kotlinx.serialization.cbor.Cbor; +import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.http.codec.KotlinSerializationBinaryDecoder; @@ -26,24 +29,62 @@ import org.springframework.http.codec.KotlinSerializationBinaryDecoder; * kotlinx.serialization. * It supports {@code application/cbor}. * - *

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

As of Spring Framework 7.0, by default it only decodes types annotated with + * {@link kotlinx.serialization.Serializable @Serializable} at type or generics + * level since it allows combined usage with other general purpose CBOR decoders + * like {@link JacksonCborDecoder} without conflicts. + * + *

Alternative constructors with a {@code Predicate} + * parameter can be used to customize this behavior. For example, + * {@code new KotlinSerializationCborDecoder(type -> true)} will decode all types + * supported by Kotlin Serialization, including unannotated Kotlin enumerations, + * numbers, characters, booleans and strings. * *

Decoding streams is not supported yet, see * kotlinx.serialization/issues/1073 * related issue. * * @author Iain Henderson + * @author Sebastien Deleuze * @since 6.0 + * @see KotlinSerializationCborEncoder */ public class KotlinSerializationCborDecoder extends KotlinSerializationBinaryDecoder { + /** + * Construct a new decoder using {@link Cbor.Default} instance which + * only decodes types annotated with {@link kotlinx.serialization.Serializable @Serializable} + * at type or generics level. + */ public KotlinSerializationCborDecoder() { this(Cbor.Default); } + /** + * Construct a new decoder using {@link Cbor.Default} instance which + * only decodes types for which the specified predicate returns {@code true}. + * @since 7.0 + */ + public KotlinSerializationCborDecoder(Predicate typePredicate) { + this(Cbor.Default, typePredicate); + } + + /** + * Construct a new decoder using the provided {@link Cbor} instance which + * only decodes types annotated with {@link kotlinx.serialization.Serializable @Serializable} + * at type or generics level. + */ public KotlinSerializationCborDecoder(Cbor cbor) { super(cbor, MediaType.APPLICATION_CBOR); } + + /** + * Construct a new decoder using the provided {@link Cbor} instance which + * only decodes types for which the specified predicate returns {@code true}. + * @since 7.0 + */ + public KotlinSerializationCborDecoder(Cbor cbor, Predicate typePredicate) { + super(cbor, typePredicate, MediaType.APPLICATION_CBOR); + } + } 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 d492c9172a3..9fd8c8dee66 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 @@ -16,8 +16,11 @@ package org.springframework.http.codec.cbor; +import java.util.function.Predicate; + import kotlinx.serialization.cbor.Cbor; +import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.http.codec.KotlinSerializationBinaryEncoder; @@ -26,21 +29,58 @@ import org.springframework.http.codec.KotlinSerializationBinaryEncoder; * kotlinx.serialization. * It supports {@code application/cbor}. * - *

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

As of Spring Framework 7.0, by default it only encodes types annotated with + * {@link kotlinx.serialization.Serializable @Serializable} at type or generics + * level since it allows combined usage with other general purpose JSON encoders + * like {@link JacksonCborEncoder} without conflicts. + * + *

Alternative constructors with a {@code Predicate} + * parameter can be used to customize this behavior. For example, + * {@code new KotlinSerializationCborEncoder(type -> true)} will encode all types + * supported by Kotlin Serialization, including unannotated Kotlin enumerations, + * numbers, characters, booleans and strings. * * @author Iain Henderson + * @author Sebastien Deleuze * @since 6.0 + * @see KotlinSerializationCborDecoder */ public class KotlinSerializationCborEncoder extends KotlinSerializationBinaryEncoder { + /** + * Construct a new encoder using {@link Cbor.Default} instance which + * only encodes types annotated with {@link kotlinx.serialization.Serializable @Serializable} + * at type or generics level. + */ public KotlinSerializationCborEncoder() { this(Cbor.Default); } + /** + * Construct a new encoder using {@link Cbor.Default} instance which + * only encodes types for which the specified predicate returns {@code true}. + * @since 7.0 + */ + public KotlinSerializationCborEncoder(Predicate typePredicate) { + this(Cbor.Default, typePredicate); + } + + /** + * Construct a new encoder using the provided {@link Cbor} instance which + * only encodes types annotated with {@link kotlinx.serialization.Serializable @Serializable} + * at type or generics level. + */ public KotlinSerializationCborEncoder(Cbor cbor) { super(cbor, MediaType.APPLICATION_CBOR); } + /** + * Construct a new encoder using the provided {@link Cbor} instance which + * only encodes types for which the specified predicate returns {@code true}. + * @since 7.0 + */ + public KotlinSerializationCborEncoder(Cbor cbor, Predicate typePredicate) { + super(cbor, typePredicate, MediaType.APPLICATION_CBOR); + } + } 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 d7d7d673226..227f320987c 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,10 +16,14 @@ package org.springframework.http.codec.json; +import java.util.function.Predicate; + import kotlinx.serialization.json.Json; +import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.http.codec.KotlinSerializationStringDecoder; +import org.springframework.util.MimeType; /** * Decode a byte stream into JSON and convert to Object's with @@ -27,9 +31,16 @@ import org.springframework.http.codec.KotlinSerializationStringDecoder; * 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. + *

As of Spring Framework 7.0, by default it only decodes types annotated with + * {@link kotlinx.serialization.Serializable @Serializable} at type or generics + * level since it allows combined usage with other general purpose JSON decoders + * like {@link JacksonJsonDecoder} without conflicts. + * + *

Alternative constructors with a {@code Predicate} + * parameter can be used to customize this behavior. For example, + * {@code new KotlinSerializationJsonDecoder(type -> true)} will decode all types + * supported by Kotlin Serialization, including unannotated Kotlin enumerations, + * numbers, characters, booleans and strings. * *

Decoding streams is not supported yet, see * kotlinx.serialization/issues/1073 @@ -38,16 +49,50 @@ import org.springframework.http.codec.KotlinSerializationStringDecoder; * @author Sebastien Deleuze * @author Iain Henderson * @since 5.3 + * @see KotlinSerializationJsonEncoder */ public class KotlinSerializationJsonDecoder extends KotlinSerializationStringDecoder { + private static final MimeType[] DEFAULT_JSON_MIME_TYPES = new MimeType[] { + MediaType.APPLICATION_JSON, + new MediaType("application", "*+json"), + MediaType.APPLICATION_NDJSON + }; + + /** + * Construct a new decoder using {@link Json.Default} instance which + * only decodes types annotated with {@link kotlinx.serialization.Serializable @Serializable} + * at type or generics level. + */ public KotlinSerializationJsonDecoder() { this(Json.Default); } + /** + * Construct a new decoder using {@link Json.Default} instance which + * only decodes types for which the specified predicate returns {@code true}. + * @since 7.0 + */ + public KotlinSerializationJsonDecoder(Predicate typePredicate) { + this(Json.Default, typePredicate); + } + + /** + * Construct a new decoder using the provided {@link Json} instance which + * only decodes types annotated with {@link kotlinx.serialization.Serializable @Serializable} + * at type or generics level. + */ public KotlinSerializationJsonDecoder(Json json) { - super(json, MediaType.APPLICATION_JSON, new MediaType("application", "*+json"), - MediaType.APPLICATION_NDJSON); + super(json, DEFAULT_JSON_MIME_TYPES); + } + + /** + * Construct a new decoder using the provided {@link Json} instance which + * only decodes types for which the specified predicate returns {@code true}. + * @since 7.0 + */ + public KotlinSerializationJsonDecoder(Json json, Predicate typePredicate) { + super(json, typePredicate, DEFAULT_JSON_MIME_TYPES); } } 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 18983b162f4..4c3e22ec054 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 @@ -18,6 +18,7 @@ package org.springframework.http.codec.json; import java.util.List; import java.util.Map; +import java.util.function.Predicate; import kotlinx.serialization.json.Json; import org.jspecify.annotations.Nullable; @@ -38,23 +39,64 @@ import org.springframework.util.MimeType; * 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. + *

As of Spring Framework 7.0, by default it only encodes types annotated with + * {@link kotlinx.serialization.Serializable @Serializable} at type or generics + * level. + * + *

Alternative constructors with a {@code Predicate} + * parameter can be used to customize this behavior. For example, + * {@code new KotlinSerializationJsonEncoder(type -> true)} will encode all types + * supported by Kotlin Serialization, including unannotated Kotlin enumerations, + * numbers, characters, booleans and strings. * * @author Sebastien Deleuze * @author Iain Henderson * @since 5.3 + * @see KotlinSerializationJsonDecoder */ public class KotlinSerializationJsonEncoder extends KotlinSerializationStringEncoder { + private static final MimeType[] DEFAULT_JSON_MIME_TYPES = new MimeType[] { + MediaType.APPLICATION_JSON, + new MediaType("application", "*+json"), + MediaType.APPLICATION_NDJSON + }; + + /** + * Construct a new encoder using {@link Json.Default} instance which + * only encodes types annotated with {@link kotlinx.serialization.Serializable @Serializable} + * at type or generics level. + */ public KotlinSerializationJsonEncoder() { this(Json.Default); } + /** + * Construct a new encoder using {@link Json.Default} instance which + * only encodes types for which the specified predicate returns {@code true}. + * @since 7.0 + */ + public KotlinSerializationJsonEncoder(Predicate typePredicate) { + this(Json.Default, typePredicate); + } + + /** + * Construct a new encoder using the provided {@link Json} instance which + * only encodes types annotated with {@link kotlinx.serialization.Serializable @Serializable} + * at type or generics level. + */ public KotlinSerializationJsonEncoder(Json json) { - super(json, MediaType.APPLICATION_JSON, new MediaType("application", "*+json"), - MediaType.APPLICATION_NDJSON); + super(json, DEFAULT_JSON_MIME_TYPES); + setStreamingMediaTypes(List.of(MediaType.APPLICATION_NDJSON)); + } + + /** + * Construct a new encoder using the provided {@link Json} instance which + * only encodes types for which the specified predicate returns {@code true}. + * @since 7.0 + */ + public KotlinSerializationJsonEncoder(Json json, Predicate typePredicate) { + super(json, typePredicate, DEFAULT_JSON_MIME_TYPES); setStreamingMediaTypes(List.of(MediaType.APPLICATION_NDJSON)); } 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 44348cde620..4ea37699591 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 @@ -16,8 +16,11 @@ package org.springframework.http.codec.protobuf; +import java.util.function.Predicate; + import kotlinx.serialization.protobuf.ProtoBuf; +import org.springframework.core.ResolvableType; import org.springframework.http.codec.KotlinSerializationBinaryDecoder; /** @@ -25,24 +28,61 @@ import org.springframework.http.codec.KotlinSerializationBinaryDecoder; * kotlinx.serialization. * 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. + *

As of Spring Framework 7.0, by default it only decodes types annotated with + * {@link kotlinx.serialization.Serializable @Serializable} at type or generics + * level. + * + *

Alternative constructors with a {@code Predicate} + * parameter can be used to customize this behavior. For example, + * {@code new KotlinSerializationProtobufDecoder(type -> true)} will decode all types + * supported by Kotlin Serialization, including unannotated Kotlin enumerations, + * numbers, characters, booleans and strings. * *

Decoding streams is not supported yet, see * kotlinx.serialization/issues/1073 * related issue. * * @author Iain Henderson + * @author Sebastien Deleuze * @since 6.0 + * @see KotlinSerializationProtobufEncoder */ public class KotlinSerializationProtobufDecoder extends KotlinSerializationBinaryDecoder { + /** + * Construct a new decoder using {@link ProtoBuf.Default} instance which + * only decodes types annotated with {@link kotlinx.serialization.Serializable @Serializable} + * at type or generics level. + */ + public KotlinSerializationProtobufDecoder() { this(ProtoBuf.Default); } + /** + * Construct a new decoder using {@link ProtoBuf.Default} instance which + * only decodes types for which the specified predicate returns {@code true}. + * @since 7.0 + */ + public KotlinSerializationProtobufDecoder(Predicate typePredicate) { + this(ProtoBuf.Default, typePredicate); + } + + /** + * Construct a new decoder using the provided {@link ProtoBuf} instance which + * only decodes types annotated with {@link kotlinx.serialization.Serializable @Serializable} + * at type or generics level. + */ public KotlinSerializationProtobufDecoder(ProtoBuf protobuf) { super(protobuf, ProtobufCodecSupport.MIME_TYPES); } + + /** + * Construct a new decoder using the provided {@link ProtoBuf} instance which + * only decodes types for which the specified predicate returns {@code true}. + * @since 7.0 + */ + public KotlinSerializationProtobufDecoder(ProtoBuf protobuf, Predicate typePredicate) { + super(protobuf, typePredicate, ProtobufCodecSupport.MIME_TYPES); + } } 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 2fcd82fd807..1530c29e709 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 @@ -16,8 +16,11 @@ package org.springframework.http.codec.protobuf; +import java.util.function.Predicate; + import kotlinx.serialization.protobuf.ProtoBuf; +import org.springframework.core.ResolvableType; import org.springframework.http.codec.KotlinSerializationBinaryEncoder; /** @@ -25,24 +28,60 @@ import org.springframework.http.codec.KotlinSerializationBinaryEncoder; * kotlinx.serialization. * 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. + *

As of Spring Framework 7.0, by default it only encodes types annotated with + * {@link kotlinx.serialization.Serializable @Serializable} at type or generics + * level. + * + *

Alternative constructors with a {@code Predicate} + * parameter can be used to customize this behavior. For example, + * {@code new KotlinSerializationProtobufEncoder(type -> true)} will encode all types + * supported by Kotlin Serialization, including unannotated Kotlin enumerations, + * numbers, characters, booleans and strings. * *

Decoding streams is not supported yet, see * kotlinx.serialization/issues/1073 * related issue. * * @author Iain Henderson + * @author Sebastien Deleuze * @since 6.0 + * @see KotlinSerializationProtobufDecoder */ public class KotlinSerializationProtobufEncoder extends KotlinSerializationBinaryEncoder { + /** + * Construct a new encoder using {@link ProtoBuf.Default} instance which + * only encodes types annotated with {@link kotlinx.serialization.Serializable @Serializable} + * at type or generics level. + */ public KotlinSerializationProtobufEncoder() { this(ProtoBuf.Default); } + /** + * Construct a new encoder using {@link ProtoBuf.Default} instance which + * only encodes types for which the specified predicate returns {@code true}. + * @since 7.0 + */ + public KotlinSerializationProtobufEncoder(Predicate typePredicate) { + this(ProtoBuf.Default, typePredicate); + } + + /** + * Construct a new encoder using the provided {@link ProtoBuf} instance which + * only encodes types annotated with {@link kotlinx.serialization.Serializable @Serializable} + * at type or generics level. + */ public KotlinSerializationProtobufEncoder(ProtoBuf protobuf) { super(protobuf, ProtobufCodecSupport.MIME_TYPES); } + + /** + * Construct a new encoder using the provided {@link ProtoBuf} instance which + * only encodes types for which the specified predicate returns {@code true}. + * @since 7.0 + */ + public KotlinSerializationProtobufEncoder(ProtoBuf protobuf, Predicate typePredicate) { + super(protobuf, typePredicate, ProtobufCodecSupport.MIME_TYPES); + } } 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 eca23093086..8d72de34dc8 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 @@ -49,22 +49,41 @@ class KotlinSerializationCborDecoderTests : AbstractDecoderTests { return Mono.defer { - val bytes = Cbor.Default.encodeToByteArray(serializer(Pojo::class.java), value) + val bytes = Cbor.encodeToByteArray(serializer(Pojo::class.java), value) val buffer = bufferFactory.allocateBuffer(bytes.size) buffer.write(bytes) Mono.just(buffer) diff --git a/spring-web/src/test/kotlin/org/springframework/http/codec/cbor/KotlinSerializationCborEncoderTests.kt b/spring-web/src/test/kotlin/org/springframework/http/codec/cbor/KotlinSerializationCborEncoderTests.kt index cc99d3003f7..3c9aa2f894d 100644 --- a/spring-web/src/test/kotlin/org/springframework/http/codec/cbor/KotlinSerializationCborEncoderTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/http/codec/cbor/KotlinSerializationCborEncoderTests.kt @@ -49,10 +49,24 @@ class KotlinSerializationCborEncoderTests : AbstractEncoderTests -> step .consumeNextWith(expectBytes(pojoBytes) diff --git a/spring-web/src/test/kotlin/org/springframework/http/codec/json/CustomKotlinSerializationJsonDecoderTests.kt b/spring-web/src/test/kotlin/org/springframework/http/codec/json/CustomKotlinSerializationJsonDecoderTests.kt index 483d06acce8..9e554d4a225 100644 --- a/spring-web/src/test/kotlin/org/springframework/http/codec/json/CustomKotlinSerializationJsonDecoderTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/http/codec/json/CustomKotlinSerializationJsonDecoderTests.kt @@ -36,7 +36,7 @@ import java.nio.charset.StandardCharsets * @author Sebastien Deleuze */ class CustomKotlinSerializationJsonDecoderTests : - AbstractDecoderTests(KotlinSerializationJsonDecoder(customJson)) { + AbstractDecoderTests(KotlinSerializationJsonDecoder(customJson) { true }) { @Test override fun canDecode() { diff --git a/spring-web/src/test/kotlin/org/springframework/http/codec/json/CustomKotlinSerializationJsonEncoderTests.kt b/spring-web/src/test/kotlin/org/springframework/http/codec/json/CustomKotlinSerializationJsonEncoderTests.kt index 037dab3b1e9..65c5ae76cbc 100644 --- a/spring-web/src/test/kotlin/org/springframework/http/codec/json/CustomKotlinSerializationJsonEncoderTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/http/codec/json/CustomKotlinSerializationJsonEncoderTests.kt @@ -34,7 +34,7 @@ import java.math.BigDecimal * @author Sebastien Deleuze */ class CustomKotlinSerializationJsonEncoderTests : - AbstractEncoderTests(KotlinSerializationJsonEncoder(customJson)) { + AbstractEncoderTests(KotlinSerializationJsonEncoder(customJson) { true }) { @Test override fun canEncode() { 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 6da9fcd2141..10689531892 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 @@ -59,13 +59,13 @@ class KotlinSerializationJsonDecoderTests : AbstractDecoderTests(KotlinSerializationJsonEncoder()) { @Test @@ -55,12 +54,51 @@ class KotlinSerializationJsonEncoderTests : AbstractEncoderTests(KotlinSerializationProtobufDecoder()) { +class KotlinSerializationProtobufDecoderTests : AbstractDecoderTests(KotlinSerializationProtobufDecoder { true }) { @Test override fun canDecode() { @@ -56,7 +56,7 @@ class KotlinSerializationProtobufDecoderTests : AbstractDecoderTests { return Mono.defer { - val bytes = ProtoBuf.Default.encodeToByteArray(serializer(Pojo::class.java), value) + val bytes = ProtoBuf.encodeToByteArray(serializer(Pojo::class.java), value) val buffer = bufferFactory.allocateBuffer(bytes.size) buffer.write(bytes) Mono.just(buffer) diff --git a/spring-web/src/test/kotlin/org/springframework/http/codec/protobuf/KotlinSerializationProtobufEncoderTests.kt b/spring-web/src/test/kotlin/org/springframework/http/codec/protobuf/KotlinSerializationProtobufEncoderTests.kt index 36a80859602..d61ab270b4c 100644 --- a/spring-web/src/test/kotlin/org/springframework/http/codec/protobuf/KotlinSerializationProtobufEncoderTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/http/codec/protobuf/KotlinSerializationProtobufEncoderTests.kt @@ -29,6 +29,7 @@ import org.springframework.core.io.buffer.DataBufferUtils import org.springframework.core.testfixture.codec.AbstractEncoderTests import org.springframework.http.MediaType import org.springframework.http.codec.ServerSentEvent +import org.springframework.http.codec.cbor.KotlinSerializationCborEncoder import reactor.core.publisher.Flux import reactor.core.publisher.Mono import reactor.test.StepVerifier.FirstStep @@ -57,14 +58,33 @@ class KotlinSerializationProtobufEncoderTests : AbstractEncoderTests -> step .consumeNextWith(expectBytes(pojoBytes) @@ -90,7 +110,7 @@ class KotlinSerializationProtobufEncoderTests : AbstractEncoderTests -> step - .consumeNextWith(expectBytes(ProtoBuf.Default.encodeToByteArray(pojo)) + .consumeNextWith(expectBytes(ProtoBuf.encodeToByteArray(pojo)) .andThen { dataBuffer: DataBuffer -> DataBufferUtils.release(dataBuffer) }) .verifyComplete() }