Skip to content

Commit 08d687f

Browse files
committed
Add serializerOrNull function for KType and Type arguments
to avoid building framework logic on catching exceptions. Fixes #1163
1 parent a9899a7 commit 08d687f

File tree

6 files changed

+196
-75
lines changed

6 files changed

+196
-75
lines changed

core/api/kotlinx-serialization-core.api

+4
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,11 @@ public final class kotlinx/serialization/SerializersKt {
101101
public static final fun serializer (Lkotlin/reflect/KType;)Lkotlinx/serialization/KSerializer;
102102
public static final fun serializer (Lkotlinx/serialization/modules/SerializersModule;Ljava/lang/reflect/Type;)Lkotlinx/serialization/KSerializer;
103103
public static final fun serializer (Lkotlinx/serialization/modules/SerializersModule;Lkotlin/reflect/KType;)Lkotlinx/serialization/KSerializer;
104+
public static final fun serializerOrNull (Ljava/lang/reflect/Type;)Lkotlinx/serialization/KSerializer;
104105
public static final fun serializerOrNull (Lkotlin/reflect/KClass;)Lkotlinx/serialization/KSerializer;
106+
public static final fun serializerOrNull (Lkotlin/reflect/KType;)Lkotlinx/serialization/KSerializer;
107+
public static final fun serializerOrNull (Lkotlinx/serialization/modules/SerializersModule;Ljava/lang/reflect/Type;)Lkotlinx/serialization/KSerializer;
108+
public static final fun serializerOrNull (Lkotlinx/serialization/modules/SerializersModule;Lkotlin/reflect/KType;)Lkotlinx/serialization/KSerializer;
105109
}
106110

107111
public abstract interface class kotlinx/serialization/StringFormat : kotlinx/serialization/SerialFormat {

core/commonMain/src/kotlinx/serialization/Serializers.kt

+43-26
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
package kotlinx.serialization
99

1010
import kotlinx.serialization.builtins.*
11-
import kotlinx.serialization.builtins.TripleSerializer
12-
import kotlinx.serialization.builtins.PairSerializer
1311
import kotlinx.serialization.builtins.MapEntrySerializer
12+
import kotlinx.serialization.builtins.PairSerializer
13+
import kotlinx.serialization.builtins.TripleSerializer
1414
import kotlinx.serialization.internal.*
1515
import kotlinx.serialization.modules.*
1616
import kotlin.jvm.*
@@ -35,48 +35,68 @@ public inline fun <reified T> SerializersModule.serializer(): KSerializer<T> {
3535
/**
3636
* Creates a serializer for the given [type].
3737
* [type] argument can be obtained with experimental [typeOf] method.
38+
* @throws SerializationException if serializer cannot be created (provided [type] or its type argument is not serializable).
3839
*/
3940
@OptIn(ExperimentalSerializationApi::class)
40-
public fun serializer(type: KType): KSerializer<Any?> {
41-
val result = EmptySerializersModule.serializerByKTypeImpl(type) ?: type.kclass().platformSpecificSerializerNotRegistered()
42-
return result.nullable(type.isMarkedNullable)
43-
}
41+
public fun serializer(type: KType): KSerializer<Any?> = EmptySerializersModule.serializer(type)
42+
43+
/**
44+
* Creates a serializer for the given [type].
45+
* [type] argument can be obtained with experimental [typeOf] method.
46+
* Returns `null` if serializer cannot be created (provided [type] or its type argument is not serializable).
47+
*/
48+
@OptIn(ExperimentalSerializationApi::class)
49+
public fun serializerOrNull(type: KType): KSerializer<Any?>? = EmptySerializersModule.serializerOrNull(type)
4450

4551
/**
4652
* Attempts to create a serializer for the given [type] and fallbacks to [contextual][SerializersModule.getContextual]
4753
* lookup for non-serializable types.
4854
* [type] argument can be obtained with experimental [typeOf] method.
55+
* @throws SerializationException if serializer cannot be created (provided [type] or its type argument is not serializable and is not registered in [this] module).
4956
*/
5057
@OptIn(ExperimentalSerializationApi::class)
51-
public fun SerializersModule.serializer(type: KType): KSerializer<Any?> {
52-
val kclass = type.kclass()
53-
val isNullable = type.isMarkedNullable
54-
val builtin = serializerByKTypeImpl(type)
55-
if (builtin != null) {
56-
return builtin.nullable(isNullable).cast()
57-
}
58+
public fun SerializersModule.serializer(type: KType): KSerializer<Any?> =
59+
serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = true) ?: type.kclass()
60+
.platformSpecificSerializerNotRegistered()
5861

59-
return getContextual(kclass)?.nullable(isNullable)?.cast() ?: type.kclass().platformSpecificSerializerNotRegistered()
62+
/**
63+
* Attempts to create a serializer for the given [type] and fallbacks to [contextual][SerializersModule.getContextual]
64+
* lookup for non-serializable types.
65+
* [type] argument can be obtained with experimental [typeOf] method.
66+
* Returns `null` if serializer cannot be created (provided [type] or its type argument is not serializable and is not registered in [this] module).
67+
*/
68+
@OptIn(ExperimentalSerializationApi::class)
69+
public fun SerializersModule.serializerOrNull(type: KType): KSerializer<Any?>? {
70+
return serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = false)
6071
}
6172

6273
@OptIn(ExperimentalSerializationApi::class)
63-
private fun SerializersModule.serializerByKTypeImpl(type: KType): KSerializer<Any>? {
74+
private fun SerializersModule.serializerByKTypeImpl(
75+
type: KType,
76+
failOnMissingTypeArgSerializer: Boolean
77+
): KSerializer<Any?>? {
6478
val rootClass = type.kclass()
79+
val isNullable = type.isMarkedNullable
6580
val typeArguments = type.arguments
6681
.map { requireNotNull(it.type) { "Star projections in type arguments are not allowed, but had $type" } }
67-
return when {
82+
val result: KSerializer<Any>? = when {
6883
typeArguments.isEmpty() -> rootClass.serializerOrNull() ?: getContextual(rootClass)
69-
else -> builtinSerializerOrNull(typeArguments, rootClass)
84+
else -> builtinSerializer(typeArguments, rootClass, failOnMissingTypeArgSerializer)
7085
}?.cast()
86+
return result?.nullable(isNullable)
7187
}
7288

7389
@OptIn(ExperimentalSerializationApi::class)
74-
private fun SerializersModule.builtinSerializerOrNull(
90+
private fun SerializersModule.builtinSerializer(
7591
typeArguments: List<KType>,
76-
rootClass: KClass<Any>
92+
rootClass: KClass<Any>,
93+
failOnMissingTypeArgSerializer: Boolean
7794
): KSerializer<out Any>? {
78-
val serializers = typeArguments
79-
.map(::serializer)
95+
val serializers = if (failOnMissingTypeArgSerializer)
96+
typeArguments.map(::serializer)
97+
else {
98+
typeArguments.map { serializerOrNull(it) ?: return null }
99+
}
80100
// Array is not supported, see KT-32839
81101
return when (rootClass) {
82102
Collection::class, List::class, MutableList::class, ArrayList::class -> ArrayListSerializer(serializers[0])
@@ -94,10 +114,7 @@ private fun SerializersModule.builtinSerializerOrNull(
94114
if (isReferenceArray(rootClass)) {
95115
return ArraySerializer<Any, Any?>(typeArguments[0].classifier as KClass<Any>, serializers[0]).cast()
96116
}
97-
requireNotNull(rootClass.constructSerializerForGivenTypeArgs(*serializers.toTypedArray())) {
98-
"Can't find a method to construct serializer for type ${rootClass.simpleName}. " +
99-
"Make sure this class is marked as @Serializable or provide serializer explicitly."
100-
}
117+
rootClass.constructSerializerForGivenTypeArgs(*serializers.toTypedArray())
101118
}
102119
}
103120
}
@@ -152,7 +169,7 @@ public fun <T : Any> KClass<T>.serializer(): KSerializer<T> = serializerOrNull()
152169
public fun <T : Any> KClass<T>.serializerOrNull(): KSerializer<T>? =
153170
compiledSerializerImpl() ?: builtinSerializerOrNull()
154171

155-
private fun <T: Any> KSerializer<T>.nullable(shouldBeNullable: Boolean): KSerializer<T?> {
172+
private fun <T : Any> KSerializer<T>.nullable(shouldBeNullable: Boolean): KSerializer<T?> {
156173
if (shouldBeNullable) return nullable
157174
return this as KSerializer<T?>
158175
}

core/jvmMain/src/kotlinx/serialization/SerializersJvm.kt

+99-45
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,31 @@ import java.lang.reflect.*
1717
import kotlin.reflect.*
1818

1919
/**
20-
* Constructs a serializer for the given reflective Java [type].
20+
* Reflectively constructs a serializer for the given reflective Java [type].
2121
* [serializer] is intended to be used as an interoperability layer for libraries like GSON and Retrofit,
2222
* that operate with reflective Java [Type] and cannot use [typeOf].
2323
*
2424
* For application-level serialization, it is recommended to use `serializer<T>()` instead as it is aware of
25-
* Kotlin-specific type information, such as nullability, sealed classes and object.
25+
* Kotlin-specific type information, such as nullability, sealed classes and object singletons.
26+
*
27+
* @throws SerializationException if serializer cannot be created (provided [type] or its type argument is not serializable).
2628
*/
2729
@ExperimentalSerializationApi
2830
public fun serializer(type: Type): KSerializer<Any> = EmptySerializersModule.serializer(type)
2931

32+
/**
33+
* Reflectively constructs a serializer for the given reflective Java [type].
34+
* [serializer] is intended to be used as an interoperability layer for libraries like GSON and Retrofit,
35+
* that operate with reflective Java [Type] and cannot use [typeOf].
36+
*
37+
* For application-level serialization, it is recommended to use `serializer<T>()` instead as it is aware of
38+
* Kotlin-specific type information, such as nullability, sealed classes and object singletons.
39+
*
40+
* Returns `null` if serializer cannot be created (provided [type] or its type argument is not serializable).
41+
*/
42+
@ExperimentalSerializationApi
43+
public fun serializerOrNull(type: Type): KSerializer<Any>? = EmptySerializersModule.serializerOrNull(type)
44+
3045
/**
3146
* Retrieves serializer for the given reflective Java [type] using
3247
* reflective construction and [contextual][SerializersModule.getContextual] lookup for non-serializable types.
@@ -36,71 +51,101 @@ public fun serializer(type: Type): KSerializer<Any> = EmptySerializersModule.ser
3651
*
3752
* For application-level serialization, it is recommended to use `serializer<T>()` instead as it is aware of
3853
* Kotlin-specific type information, such as nullability, sealed classes and object singletons.
54+
*
55+
* @throws SerializationException if serializer cannot be created (provided [type] or its type argument is not serializable).
3956
*/
4057
@ExperimentalSerializationApi
41-
public fun SerializersModule.serializer(type: Type): KSerializer<Any> = when (type) {
42-
is GenericArrayType -> {
43-
genericArraySerializer(type)
44-
}
45-
is Class<*> -> typeSerializer(type)
46-
is ParameterizedType -> {
47-
val rootClass = (type.rawType as Class<*>)
48-
val args = (type.actualTypeArguments)
49-
when {
50-
Set::class.java.isAssignableFrom(rootClass) -> SetSerializer(serializer(args[0])) as KSerializer<Any>
51-
List::class.java.isAssignableFrom(rootClass) || Collection::class.java.isAssignableFrom(rootClass) -> ListSerializer(serializer(args[0])) as KSerializer<Any>
52-
Map::class.java.isAssignableFrom(rootClass) -> MapSerializer(
53-
serializer(args[0]),
54-
serializer(args[1])
55-
) as KSerializer<Any>
56-
Map.Entry::class.java.isAssignableFrom(rootClass) -> MapEntrySerializer(
57-
serializer(args[0]),
58-
serializer(args[1])
59-
) as KSerializer<Any>
60-
Pair::class.java.isAssignableFrom(rootClass) -> PairSerializer(
61-
serializer(args[0]),
62-
serializer(args[1])
63-
) as KSerializer<Any>
64-
Triple::class.java.isAssignableFrom(rootClass) -> TripleSerializer(
65-
serializer(args[0]),
66-
serializer(args[1]),
67-
serializer(args[2])
68-
) as KSerializer<Any>
58+
public fun SerializersModule.serializer(type: Type): KSerializer<Any> =
59+
serializerByJavaTypeImpl(type, failOnMissingTypeArgSerializer = true) ?: type.kclass().serializerNotRegistered()
6960

70-
else -> {
71-
// probably we should deprecate this method because it can't differ nullable vs non-nullable types
72-
// since it uses Java TypeToken, not Kotlin one
73-
val varargs = args.map { serializer(it) as KSerializer<Any?> }.toTypedArray()
74-
(rootClass.kotlin.constructSerializerForGivenTypeArgs(*varargs) as? KSerializer<Any>)
75-
?: reflectiveOrContextual(rootClass.kotlin as KClass<Any>)
61+
/**
62+
* Retrieves serializer for the given reflective Java [type] using
63+
* reflective construction and [contextual][SerializersModule.getContextual] lookup for non-serializable types.
64+
*
65+
* [serializer] is intended to be used as an interoperability layer for libraries like GSON and Retrofit,
66+
* that operate with reflective Java [Type] and cannot use [typeOf].
67+
*
68+
* For application-level serialization, it is recommended to use `serializer<T>()` instead as it is aware of
69+
* Kotlin-specific type information, such as nullability, sealed classes and object singletons.
70+
*
71+
* Returns `null` if serializer cannot be created (provided [type] or its type argument is not serializable).
72+
*/
73+
@ExperimentalSerializationApi
74+
public fun SerializersModule.serializerOrNull(type: Type): KSerializer<Any>? =
75+
serializerByJavaTypeImpl(type, failOnMissingTypeArgSerializer = false)
76+
77+
@OptIn(ExperimentalSerializationApi::class)
78+
private fun SerializersModule.serializerByJavaTypeImpl(type: Type, failOnMissingTypeArgSerializer: Boolean = true): KSerializer<Any>? =
79+
when (type) {
80+
is GenericArrayType -> {
81+
genericArraySerializer(type, failOnMissingTypeArgSerializer)
82+
}
83+
is Class<*> -> typeSerializer(type, failOnMissingTypeArgSerializer)
84+
is ParameterizedType -> {
85+
val rootClass = (type.rawType as Class<*>)
86+
val args = (type.actualTypeArguments)
87+
val argsSerializers =
88+
if (failOnMissingTypeArgSerializer) args.map { serializer(it) } else args.map { serializerOrNull(it) ?: return null }
89+
when {
90+
Set::class.java.isAssignableFrom(rootClass) -> SetSerializer(argsSerializers[0]) as KSerializer<Any>
91+
List::class.java.isAssignableFrom(rootClass) || Collection::class.java.isAssignableFrom(rootClass) -> ListSerializer(
92+
argsSerializers[0]
93+
) as KSerializer<Any>
94+
Map::class.java.isAssignableFrom(rootClass) -> MapSerializer(
95+
argsSerializers[0],
96+
argsSerializers[1]
97+
) as KSerializer<Any>
98+
Map.Entry::class.java.isAssignableFrom(rootClass) -> MapEntrySerializer(
99+
argsSerializers[0],
100+
argsSerializers[1]
101+
) as KSerializer<Any>
102+
Pair::class.java.isAssignableFrom(rootClass) -> PairSerializer(
103+
argsSerializers[0],
104+
argsSerializers[1]
105+
) as KSerializer<Any>
106+
Triple::class.java.isAssignableFrom(rootClass) -> TripleSerializer(
107+
argsSerializers[0],
108+
argsSerializers[1],
109+
argsSerializers[2]
110+
) as KSerializer<Any>
111+
112+
else -> {
113+
// probably we should deprecate this method because it can't differ nullable vs non-nullable types
114+
// since it uses Java TypeToken, not Kotlin one
115+
val varargs = argsSerializers.map { it as KSerializer<Any?> }.toTypedArray()
116+
(rootClass.kotlin.constructSerializerForGivenTypeArgs(*varargs) as? KSerializer<Any>)
117+
?: reflectiveOrContextual(rootClass.kotlin as KClass<Any>)
118+
}
76119
}
77120
}
121+
is WildcardType -> serializerByJavaTypeImpl(type.upperBounds.first())
122+
else -> throw IllegalArgumentException("typeToken should be an instance of Class<?>, GenericArray, ParametrizedType or WildcardType, but actual type is $type ${type::class}")
78123
}
79-
is WildcardType -> serializer(type.upperBounds.first())
80-
else -> throw IllegalArgumentException("typeToken should be an instance of Class<?>, GenericArray, ParametrizedType or WildcardType, but actual type is $type ${type::class}")
81-
}
82124

83125
@OptIn(ExperimentalSerializationApi::class)
84-
private fun SerializersModule.typeSerializer(type: Class<*>): KSerializer<Any> {
126+
private fun SerializersModule.typeSerializer(type: Class<*>, failOnMissingTypeArgSerializer: Boolean): KSerializer<Any>? {
85127
return if (!type.isArray) {
86128
reflectiveOrContextual(type.kotlin as KClass<Any>)
87129
} else {
88130
val eType: Class<*> = type.componentType
89-
val s = serializer(eType)
131+
val s = if (failOnMissingTypeArgSerializer) serializer(eType) else (serializerOrNull(eType) ?: return null)
90132
val arraySerializer = ArraySerializer(eType.kotlin as KClass<Any>, s)
91133
arraySerializer as KSerializer<Any>
92134
}
93135
}
94136

95137
@OptIn(ExperimentalSerializationApi::class)
96-
private fun SerializersModule.genericArraySerializer(type: GenericArrayType): KSerializer<Any> {
138+
private fun SerializersModule.genericArraySerializer(
139+
type: GenericArrayType,
140+
failOnMissingTypeArgSerializer: Boolean
141+
): KSerializer<Any>? {
97142
val eType = type.genericComponentType.let {
98143
when (it) {
99144
is WildcardType -> it.upperBounds.first()
100145
else -> it
101146
}
102147
}
103-
val serializer = serializer(eType)
148+
val serializer = if (failOnMissingTypeArgSerializer) serializer(eType) else (serializerOrNull(eType) ?: return null)
104149
val kclass = when (eType) {
105150
is ParameterizedType -> (eType.rawType as Class<*>).kotlin
106151
is KClass<*> -> eType
@@ -110,6 +155,15 @@ private fun SerializersModule.genericArraySerializer(type: GenericArrayType): KS
110155
}
111156

112157
@OptIn(ExperimentalSerializationApi::class)
113-
private fun <T: Any> SerializersModule.reflectiveOrContextual(kClass: KClass<T>): KSerializer<T> {
114-
return kClass.serializerOrNull() ?: getContextual(kClass) ?: kClass.serializerNotRegistered()
158+
private fun <T : Any> SerializersModule.reflectiveOrContextual(kClass: KClass<T>): KSerializer<T>? {
159+
return kClass.serializerOrNull() ?: getContextual(kClass)
160+
}
161+
162+
private fun Type.kclass(): KClass<*> = when (val it = this) {
163+
is KClass<*> -> it
164+
is Class<*> -> it.kotlin
165+
is ParameterizedType -> it.rawType.kclass()
166+
is WildcardType -> it.upperBounds.first().kclass()
167+
is GenericArrayType -> it.genericComponentType.kclass()
168+
else -> throw IllegalArgumentException("typeToken should be an instance of Class<?>, GenericArray, ParametrizedType or WildcardType, but actual type is $it ${it::class}")
115169
}

formats/json/commonTest/src/kotlinx/serialization/SerializersLookupTest.kt

+23
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,29 @@ class SerializersLookupTest : JsonTestBase() {
212212
assertEquals("42", json.encodeToString(42))
213213
}
214214

215+
class NonSerializable
216+
217+
class NonSerializableBox<T>(val boxed: T)
218+
219+
@Test
220+
fun testLookupFail() {
221+
assertNull(serializerOrNull(typeOf<NonSerializable>()))
222+
assertNull(serializerOrNull(typeOf<NonSerializableBox<String>>()))
223+
assertNull(serializerOrNull(typeOf<Box<NonSerializable>>()))
224+
225+
assertFailsWithMessage<SerializationException>("for class 'NonSerializable'") {
226+
serializer(typeOf<NonSerializable>())
227+
}
228+
229+
assertFailsWithMessage<SerializationException>("for class 'NonSerializableBox'") {
230+
serializer(typeOf<NonSerializableBox<String>>())
231+
}
232+
233+
assertFailsWithMessage<SerializationException>("for class 'NonSerializable'") {
234+
serializer(typeOf<Box<NonSerializable>>())
235+
}
236+
}
237+
215238
// Tests with [constructSerializerForGivenTypeArgs] are unsupported on legacy Kotlin/JS
216239
private inline fun noLegacyJs(test: () -> Unit) {
217240
if (!isJsLegacy()) test()

formats/json/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,9 @@ abstract class JsonTestBase {
9696
assertEquals(data, deserialized)
9797
}
9898
}
99+
}
99100

100-
inline fun <reified T : Throwable> assertFailsWithMessage(message: String, block: () -> Unit) {
101-
val exception = assertFailsWith(T::class, null, block)
102-
assertTrue(exception.message!!.contains(message), "Expected message '${exception.message}' to contain substring '$message'")
103-
}
101+
inline fun <reified T : Throwable> assertFailsWithMessage(message: String, block: () -> Unit) {
102+
val exception = assertFailsWith(T::class, null, block)
103+
assertTrue(exception.message!!.contains(message), "Expected message '${exception.message}' to contain substring '$message'")
104104
}

0 commit comments

Comments
 (0)