Skip to content

Commit d64edc0

Browse files
committed
Prevent Kotlin Serialization converters side effects
This commit updates Kotlin serialization converters 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 converters like Jackson and Kotlin serialization when both are used. New constructors allowing to specify a custom predicate are also introduced. See gh-35761
1 parent d0f5701 commit d64edc0

10 files changed

+421
-90
lines changed

spring-web/src/main/java/org/springframework/http/converter/AbstractKotlinSerializationHttpMessageConverter.java

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@
2020
import java.lang.reflect.Type;
2121
import java.util.List;
2222
import java.util.Map;
23+
import java.util.function.Predicate;
2324

2425
import kotlin.reflect.KType;
2526
import kotlinx.serialization.KSerializer;
2627
import kotlinx.serialization.SerialFormat;
2728
import kotlinx.serialization.SerializersKt;
2829
import org.jspecify.annotations.Nullable;
2930

31+
import org.springframework.core.KotlinDetector;
3032
import org.springframework.core.ResolvableType;
3133
import org.springframework.http.HttpInputMessage;
3234
import org.springframework.http.HttpOutputMessage;
@@ -38,9 +40,11 @@
3840
* Abstract base class for {@link HttpMessageConverter} implementations that
3941
* use Kotlin serialization.
4042
*
41-
* <p>As of Spring Framework 7.0,
42-
* <a href="https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#open-polymorphism">open polymorphism</a>
43-
* is supported.
43+
* <p>As of Spring Framework 7.0, by default it only encodes types annotated with
44+
* {@link kotlinx.serialization.Serializable @Serializable} at type or generics level
45+
* since it allows combined usage with other general purpose converters without conflicts.
46+
* Alternative constructors with a {@code Predicate<ResolvableType>} parameter can be used
47+
* to customize this behavior.
4448
*
4549
* @author Andreas Ahlenstorf
4650
* @author Sebastien Deleuze
@@ -58,15 +62,30 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends
5862

5963
private final T format;
6064

65+
private final Predicate<ResolvableType> typePredicate;
66+
6167

6268
/**
63-
* Construct an {@code AbstractKotlinSerializationHttpMessageConverter} with multiple supported media type and
64-
* format.
65-
* @param format the format
66-
* @param supportedMediaTypes the supported media types
69+
* Creates a new instance with the given format and supported mime types
70+
* which only converters types annotated with
71+
* {@link kotlinx.serialization.Serializable @Serializable} at type or
72+
* generics level.
6773
*/
6874
protected AbstractKotlinSerializationHttpMessageConverter(T format, MediaType... supportedMediaTypes) {
6975
super(supportedMediaTypes);
76+
this.typePredicate = KotlinDetector::hasSerializableAnnotation;
77+
this.format = format;
78+
}
79+
80+
/**
81+
* Creates a new instance with the given format and supported mime types
82+
* which only converts types for which the specified predicate returns
83+
* {@code true}.
84+
* @since 7.0
85+
*/
86+
protected AbstractKotlinSerializationHttpMessageConverter(T format, Predicate<ResolvableType> typePredicate, MediaType... supportedMediaTypes) {
87+
super(supportedMediaTypes);
88+
this.typePredicate = typePredicate;
7089
this.format = format;
7190
}
7291

@@ -77,27 +96,27 @@ public List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
7796

7897
@Override
7998
protected boolean supports(Class<?> clazz) {
80-
return serializer(ResolvableType.forClass(clazz), null) != null;
99+
ResolvableType type = ResolvableType.forClass(clazz);
100+
if (!this.typePredicate.test(type)) {
101+
return false;
102+
}
103+
return serializer(type, null) != null;
81104
}
82105

83106
@Override
84107
public boolean canRead(ResolvableType type, @Nullable MediaType mediaType) {
85-
if (!ResolvableType.NONE.equals(type) && serializer(type, null) != null) {
86-
return canRead(mediaType);
87-
}
88-
else {
108+
if (!this.typePredicate.test(type) || ResolvableType.NONE.equals(type)) {
89109
return false;
90110
}
111+
return serializer(type, null) != null && canRead(mediaType);
91112
}
92113

93114
@Override
94115
public boolean canWrite(ResolvableType type, Class<?> clazz, @Nullable MediaType mediaType) {
95-
if (!ResolvableType.NONE.equals(type) && serializer(type, null) != null) {
96-
return canWrite(mediaType);
97-
}
98-
else {
116+
if (!this.typePredicate.test(type) || ResolvableType.NONE.equals(type)) {
99117
return false;
100118
}
119+
return serializer(type, null) != null && canWrite(mediaType);
101120
}
102121

103122
@Override

spring-web/src/main/java/org/springframework/http/converter/KotlinSerializationBinaryHttpMessageConverter.java

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717
package org.springframework.http.converter;
1818

1919
import java.io.IOException;
20+
import java.util.function.Predicate;
2021

2122
import kotlinx.serialization.BinaryFormat;
2223
import kotlinx.serialization.KSerializer;
2324
import kotlinx.serialization.SerializationException;
2425

26+
import org.springframework.core.ResolvableType;
2527
import org.springframework.http.HttpInputMessage;
2628
import org.springframework.http.HttpOutputMessage;
2729
import org.springframework.http.MediaType;
@@ -31,9 +33,11 @@
3133
* Abstract base class for {@link HttpMessageConverter} implementations that
3234
* defer to Kotlin {@linkplain BinaryFormat binary serializers}.
3335
*
34-
* <p>As of Spring Framework 7.0,
35-
* <a href="https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#open-polymorphism">open polymorphism</a>
36-
* is supported.
36+
* <p>As of Spring Framework 7.0, by default it only encodes types annotated with
37+
* {@link kotlinx.serialization.Serializable @Serializable} at type or generics level
38+
* since it allows combined usage with other general purpose converters without conflicts.
39+
* Alternative constructors with a {@code Predicate<ResolvableType>} parameter can be used
40+
* to customize this behavior.
3741
*
3842
* @author Andreas Ahlenstorf
3943
* @author Sebastien Deleuze
@@ -47,12 +51,25 @@ public abstract class KotlinSerializationBinaryHttpMessageConverter<T extends Bi
4751
extends AbstractKotlinSerializationHttpMessageConverter<T> {
4852

4953
/**
50-
* Construct an {@code KotlinSerializationBinaryHttpMessageConverter} with format and supported media types.
54+
* Creates a new instance with the given format and supported mime types
55+
* which only converters types annotated with
56+
* {@link kotlinx.serialization.Serializable @Serializable} at type or
57+
* generics level.
5158
*/
5259
protected KotlinSerializationBinaryHttpMessageConverter(T format, MediaType... supportedMediaTypes) {
5360
super(format, supportedMediaTypes);
5461
}
5562

63+
/**
64+
* Creates a new instance with the given format and supported mime types
65+
* which only converts types for which the specified predicate returns
66+
* {@code true}.
67+
* @since 7.0
68+
*/
69+
protected KotlinSerializationBinaryHttpMessageConverter(T format, Predicate<ResolvableType> typePredicate, MediaType... supportedMediaTypes) {
70+
super(format, typePredicate, supportedMediaTypes);
71+
}
72+
5673
@Override
5774
protected Object readInternal(KSerializer<Object> serializer, T format, HttpInputMessage inputMessage)
5875
throws IOException, HttpMessageNotReadableException {

spring-web/src/main/java/org/springframework/http/converter/KotlinSerializationStringHttpMessageConverter.java

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@
1919
import java.io.IOException;
2020
import java.nio.charset.Charset;
2121
import java.nio.charset.StandardCharsets;
22+
import java.util.function.Predicate;
2223

2324
import kotlinx.serialization.KSerializer;
2425
import kotlinx.serialization.SerializationException;
2526
import kotlinx.serialization.StringFormat;
2627
import org.jspecify.annotations.Nullable;
2728

29+
import org.springframework.core.ResolvableType;
2830
import org.springframework.http.HttpInputMessage;
2931
import org.springframework.http.HttpOutputMessage;
3032
import org.springframework.http.MediaType;
@@ -34,9 +36,11 @@
3436
* Abstract base class for {@link HttpMessageConverter} implementations that
3537
* defer to Kotlin {@linkplain StringFormat string serializers}.
3638
*
37-
* <p>As of Spring Framework 7.0,
38-
* <a href="https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#open-polymorphism">open polymorphism</a>
39-
* is supported.
39+
* <p>As of Spring Framework 7.0, by default it only encodes types annotated with
40+
* {@link kotlinx.serialization.Serializable @Serializable} at type or generics level
41+
* since it allows combined usage with other general purpose converters without conflicts.
42+
* Alternative constructors with a {@code Predicate<ResolvableType>} parameter can be used
43+
* to customize this behavior.
4044
*
4145
* @author Andreas Ahlenstorf
4246
* @author Sebastien Deleuze
@@ -51,12 +55,25 @@ public abstract class KotlinSerializationStringHttpMessageConverter<T extends St
5155

5256

5357
/**
54-
* Construct an {@code KotlinSerializationStringHttpMessageConverter} with format and supported media types.
58+
* Creates a new instance with the given format and supported mime types
59+
* which only converters types annotated with
60+
* {@link kotlinx.serialization.Serializable @Serializable} at type or
61+
* generics level.
5562
*/
5663
protected KotlinSerializationStringHttpMessageConverter(T format, MediaType... supportedMediaTypes) {
5764
super(format, supportedMediaTypes);
5865
}
5966

67+
/**
68+
* Creates a new instance with the given format and supported mime types
69+
* which only converts types for which the specified predicate returns
70+
* {@code true}.
71+
* @since 7.0
72+
*/
73+
protected KotlinSerializationStringHttpMessageConverter(T format, Predicate<ResolvableType> typePredicate, MediaType... supportedMediaTypes) {
74+
super(format, typePredicate, supportedMediaTypes);
75+
}
76+
6077

6178
@Override
6279
protected Object readInternal(KSerializer<Object> serializer, T format, HttpInputMessage inputMessage)

spring-web/src/main/java/org/springframework/http/converter/cbor/KotlinSerializationCborHttpMessageConverter.java

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616

1717
package org.springframework.http.converter.cbor;
1818

19+
import java.util.function.Predicate;
20+
1921
import kotlinx.serialization.cbor.Cbor;
2022

23+
import org.springframework.core.ResolvableType;
2124
import org.springframework.http.MediaType;
2225
import org.springframework.http.converter.KotlinSerializationBinaryHttpMessageConverter;
2326

@@ -27,20 +30,56 @@
2730
* <a href="https://github.com/Kotlin/kotlinx.serialization">kotlinx.serialization</a>.
2831
* It supports {@code application/cbor}.
2932
*
30-
* <p>As of Spring Framework 7.0,
31-
* <a href="https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#open-polymorphism">open polymorphism</a>
32-
* is supported.
33+
* <p>As of Spring Framework 7.0, by default it only types annotated with
34+
* {@link kotlinx.serialization.Serializable @Serializable} at type or generics
35+
* level since it allows combined usage with other general purpose JSON decoders
36+
* like {@link JacksonCborHttpMessageConverter} without conflicts.
37+
*
38+
* <p>Alternative constructors with a {@code Predicate<ResolvableType>}
39+
* parameter can be used to customize this behavior. For example,
40+
* {@code new KotlinSerializationCborHttpMessageConverter(type -> true)} will decode all types
41+
* supported by Kotlin Serialization, including unannotated Kotlin enumerations,
42+
* numbers, characters, booleans and strings.
3343
*
3444
* @author Iain Henderson
45+
* @author Sebastien Deleuze
3546
* @since 6.0
3647
*/
3748
public class KotlinSerializationCborHttpMessageConverter extends KotlinSerializationBinaryHttpMessageConverter<Cbor> {
49+
50+
/**
51+
* Construct a new converter using {@link Cbor.Default} instance which
52+
* only converts types annotated with {@link kotlinx.serialization.Serializable @Serializable}
53+
* at type or generics level.
54+
*/
3855
public KotlinSerializationCborHttpMessageConverter() {
3956
this(Cbor.Default);
4057
}
4158

59+
/**
60+
* Construct a new converter using {@link Cbor.Default} instance which
61+
* only converts types for which the specified predicate returns {@code true}.
62+
* @since 7.0
63+
*/
64+
public KotlinSerializationCborHttpMessageConverter(Predicate<ResolvableType> typePredicate) {
65+
this(Cbor.Default, typePredicate);
66+
}
67+
68+
/**
69+
* Construct a new converter using the provided {@link Cbor} instance which
70+
* only converts types annotated with {@link kotlinx.serialization.Serializable @Serializable}
71+
* at type or generics level.
72+
*/
4273
public KotlinSerializationCborHttpMessageConverter(Cbor cbor) {
4374
super(cbor, MediaType.APPLICATION_CBOR);
4475
}
4576

77+
/**
78+
* Construct a new converter using the provided {@link Cbor} instance which
79+
* only converts types for which the specified predicate returns {@code true}.
80+
* @since 7.0
81+
*/
82+
public KotlinSerializationCborHttpMessageConverter(Cbor cbor, Predicate<ResolvableType> typePredicate) {
83+
super(cbor, typePredicate, MediaType.APPLICATION_CBOR);
84+
}
4685
}

spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616

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

19+
import java.util.function.Predicate;
20+
1921
import kotlinx.serialization.json.Json;
2022

23+
import org.springframework.core.ResolvableType;
2124
import org.springframework.http.MediaType;
2225
import org.springframework.http.converter.KotlinSerializationStringHttpMessageConverter;
2326

@@ -28,9 +31,16 @@
2831
* It supports {@code application/json} and {@code application/*+json} with
2932
* various character sets, {@code UTF-8} being the default.
3033
*
31-
* <p>As of Spring Framework 7.0,
32-
* <a href="https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#open-polymorphism">open polymorphism</a>
33-
* is supported.
34+
* <p>As of Spring Framework 7.0, by default it only types annotated with
35+
* {@link kotlinx.serialization.Serializable @Serializable} at type or generics
36+
* level since it allows combined usage with other general purpose JSON decoders
37+
* like {@link JacksonJsonHttpMessageConverter} without conflicts.
38+
*
39+
* <p>Alternative constructors with a {@code Predicate<ResolvableType>}
40+
* parameter can be used to customize this behavior. For example,
41+
* {@code new KotlinSerializationJsonHttpMessageConverter(type -> true)} will decode all types
42+
* supported by Kotlin Serialization, including unannotated Kotlin enumerations,
43+
* numbers, characters, booleans and strings.
3444
*
3545
* @author Andreas Ahlenstorf
3646
* @author Sebastien Deleuze
@@ -40,17 +50,42 @@
4050
*/
4151
public class KotlinSerializationJsonHttpMessageConverter extends KotlinSerializationStringHttpMessageConverter<Json> {
4252

53+
private static final MediaType[] DEFAULT_JSON_MIME_TYPES = new MediaType[] {
54+
MediaType.APPLICATION_JSON, new MediaType("application", "*+json") };
55+
4356
/**
44-
* Construct a new {@code KotlinSerializationJsonHttpMessageConverter} with the default configuration.
57+
* Construct a new converter using {@link Json.Default} instance which
58+
* only converts types annotated with {@link kotlinx.serialization.Serializable @Serializable}
59+
* at type or generics level.
4560
*/
4661
public KotlinSerializationJsonHttpMessageConverter() {
4762
this(Json.Default);
4863
}
4964

5065
/**
51-
* Construct a new {@code KotlinSerializationJsonHttpMessageConverter} with a custom configuration.
66+
* Construct a new converter using {@link Json.Default} instance which
67+
* only converts types for which the specified predicate returns {@code true}.
68+
* @since 7.0
69+
*/
70+
public KotlinSerializationJsonHttpMessageConverter(Predicate<ResolvableType> typePredicate) {
71+
this(Json.Default, typePredicate);
72+
}
73+
74+
/**
75+
* Construct a new converter using the provided {@link Json} instance which
76+
* only converts types annotated with {@link kotlinx.serialization.Serializable @Serializable}
77+
* at type or generics level.
5278
*/
5379
public KotlinSerializationJsonHttpMessageConverter(Json json) {
54-
super(json, MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
80+
super(json, DEFAULT_JSON_MIME_TYPES);
81+
}
82+
83+
/**
84+
* Construct a new converter using the provided {@link Json} instance which
85+
* only converts types for which the specified predicate returns {@code true}.
86+
* @since 7.0
87+
*/
88+
public KotlinSerializationJsonHttpMessageConverter(Json json, Predicate<ResolvableType> typePredicate) {
89+
super(json, typePredicate, DEFAULT_JSON_MIME_TYPES);
5590
}
5691
}

0 commit comments

Comments
 (0)