Skip to content

Commit a27e86f

Browse files
Function to retrieve KSerializer by KClass and type arguments serializers (#2291)
Implemented obtainment of KSerializer by KClass in SerializersModule Resolves #2025 The limitations of this API are the inability to implement stable caching, because serialization runtime does not control the equals function of the received parameters serializers, which can cause a memory leak. Also, a technical limitation is the inability to create an array serializer. Co-authored-by: Leonid Startsev <sandwwraith@gmail.com>
1 parent 5a8795a commit a27e86f

File tree

6 files changed

+226
-13
lines changed

6 files changed

+226
-13
lines changed

core/api/kotlinx-serialization-core.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,10 @@ public final class kotlinx/serialization/SerializersKt {
124124
public static final fun noCompiledSerializer (Lkotlinx/serialization/modules/SerializersModule;Lkotlin/reflect/KClass;[Lkotlinx/serialization/KSerializer;)Lkotlinx/serialization/KSerializer;
125125
public static final fun serializer (Ljava/lang/reflect/Type;)Lkotlinx/serialization/KSerializer;
126126
public static final fun serializer (Lkotlin/reflect/KClass;)Lkotlinx/serialization/KSerializer;
127+
public static final fun serializer (Lkotlin/reflect/KClass;Ljava/util/List;Z)Lkotlinx/serialization/KSerializer;
127128
public static final fun serializer (Lkotlin/reflect/KType;)Lkotlinx/serialization/KSerializer;
128129
public static final fun serializer (Lkotlinx/serialization/modules/SerializersModule;Ljava/lang/reflect/Type;)Lkotlinx/serialization/KSerializer;
130+
public static final fun serializer (Lkotlinx/serialization/modules/SerializersModule;Lkotlin/reflect/KClass;Ljava/util/List;Z)Lkotlinx/serialization/KSerializer;
129131
public static final fun serializer (Lkotlinx/serialization/modules/SerializersModule;Lkotlin/reflect/KType;)Lkotlinx/serialization/KSerializer;
130132
public static final fun serializerOrNull (Ljava/lang/reflect/Type;)Lkotlinx/serialization/KSerializer;
131133
public static final fun serializerOrNull (Lkotlin/reflect/KClass;)Lkotlinx/serialization/KSerializer;

core/build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
2+
13
/*
24
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
35
*/
@@ -63,4 +65,10 @@ tasks.withType(Jar).named(kotlin.jvm().artifactsTaskName) {
6365
}
6466
}
6567

68+
tasks.withType(Kotlin2JsCompile.class).configureEach {
69+
if (it.name == "compileTestKotlinJsLegacy") {
70+
it.exclude("**/SerializersModuleTest.kt")
71+
}
72+
}
73+
6674
Java9Modularity.configureJava9ModuleInfo(project)

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

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,31 @@ public inline fun <reified T> SerializersModule.serializer(): KSerializer<T> {
7171
*/
7272
public fun serializer(type: KType): KSerializer<Any?> = EmptySerializersModule().serializer(type)
7373

74+
75+
/**
76+
* Retrieves serializer for the given [kClass].
77+
* This method uses platform-specific reflection available.
78+
*
79+
* If [kClass] is a parametrized type then it is necessary to pass serializers for generic parameters in the [typeArgumentsSerializers].
80+
* The nullability of returned serializer is specified using the [isNullable].
81+
*
82+
* Note that it is impossible to create an array serializer with this method,
83+
* as array serializer needs additional information: type token for an element type.
84+
* To create array serializer, use overload with [KType] or [ArraySerializer] directly.
85+
*
86+
* Caching on JVM platform is disabled for this function, so it may work slower than an overload with [KType].
87+
*
88+
* @throws SerializationException if serializer cannot be created (provided [kClass] or its type argument is not serializable)
89+
* @throws SerializationException if [kClass] is a `kotlin.Array`
90+
* @throws SerializationException if size of [typeArgumentsSerializers] does not match the expected generic parameters count
91+
*/
92+
@ExperimentalSerializationApi
93+
public fun serializer(
94+
kClass: KClass<*>,
95+
typeArgumentsSerializers: List<KSerializer<*>>,
96+
isNullable: Boolean
97+
): KSerializer<Any?> = EmptySerializersModule().serializer(kClass, typeArgumentsSerializers, isNullable)
98+
7499
/**
75100
* Creates a serializer for the given [type] if possible.
76101
* [type] argument is usually obtained with [typeOf] method.
@@ -108,6 +133,34 @@ public fun SerializersModule.serializer(type: KType): KSerializer<Any?> =
108133
serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = true) ?: type.kclass()
109134
.platformSpecificSerializerNotRegistered()
110135

136+
137+
/**
138+
* Retrieves serializer for the given [kClass] and,
139+
* if [kClass] is not serializable, fallbacks to [contextual][SerializersModule.getContextual] lookup.
140+
* This method uses platform-specific reflection available.
141+
*
142+
* If [kClass] is a parametrized type then it is necessary to pass serializers for generic parameters in the [typeArgumentsSerializers].
143+
* The nullability of returned serializer is specified using the [isNullable].
144+
*
145+
* Note that it is impossible to create an array serializer with this method,
146+
* as array serializer needs additional information: type token for an element type.
147+
* To create array serializer, use overload with [KType] or [ArraySerializer] directly.
148+
*
149+
* Caching on JVM platform is disabled for this function, so it may work slower than an overload with [KType].
150+
*
151+
* @throws SerializationException if serializer cannot be created (provided [kClass] or its type argument is not serializable and is not registered in [this] module)
152+
* @throws SerializationException if [kClass] is a `kotlin.Array`
153+
* @throws SerializationException if size of [typeArgumentsSerializers] does not match the expected generic parameters count
154+
*/
155+
@ExperimentalSerializationApi
156+
public fun SerializersModule.serializer(
157+
kClass: KClass<*>,
158+
typeArgumentsSerializers: List<KSerializer<*>>,
159+
isNullable: Boolean
160+
): KSerializer<Any?> =
161+
serializerByKClassImpl(kClass as KClass<Any>, typeArgumentsSerializers as List<KSerializer<Any?>>, isNullable)
162+
?: kClass.platformSpecificSerializerNotRegistered()
163+
111164
/**
112165
* Retrieves default serializer for the given [type] and,
113166
* if [type] is not serializable, fallbacks to [contextual][SerializersModule.getContextual] lookup.
@@ -156,14 +209,39 @@ private fun SerializersModule.serializerByKTypeImpl(
156209
} else {
157210
val serializers = serializersForParameters(typeArguments, failOnMissingTypeArgSerializer) ?: return null
158211
// first, we look among the built-in serializers, because the parameter could be contextual
159-
rootClass.parametrizedSerializerOrNull(typeArguments, serializers) ?: getContextual(
160-
rootClass,
161-
serializers
162-
)
212+
rootClass.parametrizedSerializerOrNull(serializers) { typeArguments[0].classifier }
213+
?: getContextual(
214+
rootClass,
215+
serializers
216+
)
163217
}
164218
return contextualSerializer?.cast<Any>()?.nullable(isNullable)
165219
}
166220

221+
@OptIn(ExperimentalSerializationApi::class)
222+
private fun SerializersModule.serializerByKClassImpl(
223+
rootClass: KClass<Any>,
224+
typeArgumentsSerializers: List<KSerializer<Any?>>,
225+
isNullable: Boolean
226+
): KSerializer<Any?>? {
227+
val serializer = if (typeArgumentsSerializers.isEmpty()) {
228+
rootClass.serializerOrNull() ?: getContextual(rootClass)
229+
} else {
230+
try {
231+
rootClass.parametrizedSerializerOrNull(typeArgumentsSerializers) {
232+
throw SerializationException("It is not possible to retrieve an array serializer using KClass alone, use KType instead or ArraySerializer factory")
233+
} ?: getContextual(
234+
rootClass,
235+
typeArgumentsSerializers
236+
)
237+
} catch (e: IndexOutOfBoundsException) {
238+
throw SerializationException("Unable to retrieve a serializer, the number of passed type serializers differs from the actual number of generic parameters", e)
239+
}
240+
}
241+
242+
return serializer?.cast<Any>()?.nullable(isNullable)
243+
}
244+
167245
/**
168246
* Returns null only if `failOnMissingTypeArgSerializer == false` and at least one parameter serializer not found.
169247
*/
@@ -230,11 +308,11 @@ public fun <T : Any> KClass<T>.serializerOrNull(): KSerializer<T>? =
230308
compiledSerializerImpl() ?: builtinSerializerOrNull()
231309

232310
internal fun KClass<Any>.parametrizedSerializerOrNull(
233-
types: List<KType>,
234-
serializers: List<KSerializer<Any?>>
311+
serializers: List<KSerializer<Any?>>,
312+
elementClassifierIfArray: () -> KClassifier?
235313
): KSerializer<out Any>? {
236314
// builtin first because some standard parametrized interfaces (e.g. Map) must use builtin serializer but not polymorphic
237-
return builtinParametrizedSerializer(types, serializers) ?: compiledParametrizedSerializer(serializers)
315+
return builtinParametrizedSerializer(serializers, elementClassifierIfArray) ?: compiledParametrizedSerializer(serializers)
238316
}
239317

240318

@@ -244,8 +322,8 @@ private fun KClass<Any>.compiledParametrizedSerializer(serializers: List<KSerial
244322

245323
@OptIn(ExperimentalSerializationApi::class)
246324
private fun KClass<Any>.builtinParametrizedSerializer(
247-
typeArguments: List<KType>,
248325
serializers: List<KSerializer<Any?>>,
326+
elementClassifierIfArray: () -> KClassifier?
249327
): KSerializer<out Any>? {
250328
return when (this) {
251329
Collection::class, List::class, MutableList::class, ArrayList::class -> ArrayListSerializer(serializers[0])
@@ -256,12 +334,13 @@ private fun KClass<Any>.builtinParametrizedSerializer(
256334
serializers[0],
257335
serializers[1]
258336
)
337+
259338
Map.Entry::class -> MapEntrySerializer(serializers[0], serializers[1])
260339
Pair::class -> PairSerializer(serializers[0], serializers[1])
261340
Triple::class -> TripleSerializer(serializers[0], serializers[1], serializers[2])
262341
else -> {
263342
if (isReferenceArray(this)) {
264-
ArraySerializer(typeArguments[0].classifier as KClass<Any>, serializers[0])
343+
ArraySerializer(elementClassifierIfArray() as KClass<Any>, serializers[0])
265344
} else {
266345
null
267346
}
@@ -297,6 +376,10 @@ internal fun noCompiledSerializer(module: SerializersModule, kClass: KClass<*>):
297376
@OptIn(ExperimentalSerializationApi::class)
298377
@Suppress("unused")
299378
@PublishedApi
300-
internal fun noCompiledSerializer(module: SerializersModule, kClass: KClass<*>, argSerializers: Array<KSerializer<*>>): KSerializer<*> {
379+
internal fun noCompiledSerializer(
380+
module: SerializersModule,
381+
kClass: KClass<*>,
382+
argSerializers: Array<KSerializer<*>>
383+
): KSerializer<*> {
301384
return module.getContextual(kClass, argSerializers.asList()) ?: kClass.serializerNotRegistered()
302385
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ private val SERIALIZERS_CACHE_NULLABLE = createCache<Any?> { it.serializerOrNull
3232
@ThreadLocal
3333
private val PARAMETRIZED_SERIALIZERS_CACHE = createParametrizedCache { clazz, types ->
3434
val serializers = EmptySerializersModule().serializersForParameters(types, true)!!
35-
clazz.parametrizedSerializerOrNull(types, serializers)
35+
clazz.parametrizedSerializerOrNull(serializers) { types[0].classifier }
3636
}
3737

3838
/**
@@ -41,7 +41,7 @@ private val PARAMETRIZED_SERIALIZERS_CACHE = createParametrizedCache { clazz, ty
4141
@ThreadLocal
4242
private val PARAMETRIZED_SERIALIZERS_CACHE_NULLABLE = createParametrizedCache<Any?> { clazz, types ->
4343
val serializers = EmptySerializersModule().serializersForParameters(types, true)!!
44-
clazz.parametrizedSerializerOrNull(types, serializers)?.nullable?.cast()
44+
clazz.parametrizedSerializerOrNull(serializers) { types[0].classifier }?.nullable?.cast()
4545
}
4646

4747
/**
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization
6+
7+
import kotlinx.serialization.builtins.*
8+
import kotlinx.serialization.descriptors.*
9+
import kotlinx.serialization.internal.*
10+
import kotlinx.serialization.modules.*
11+
import kotlinx.serialization.test.*
12+
import kotlin.reflect.*
13+
import kotlin.test.*
14+
15+
class SerializersModuleTest {
16+
@Serializable
17+
object Object
18+
19+
@Serializable
20+
sealed class SealedParent {
21+
@Serializable
22+
data class Child(val i: Int) : SealedParent()
23+
}
24+
25+
@Serializable
26+
abstract class Abstract
27+
28+
@Serializable
29+
enum class SerializableEnum { A, B }
30+
31+
@Serializable(CustomSerializer::class)
32+
class WithCustomSerializer(val i: Int)
33+
34+
@Serializer(forClass = WithCustomSerializer::class)
35+
object CustomSerializer
36+
37+
@Serializable
38+
class Parametrized<T : Any>(val a: T)
39+
40+
class ContextualType(val i: Int)
41+
42+
class ParametrizedContextual<T : Any>(val a: T)
43+
44+
@Serializer(forClass = ContextualType::class)
45+
object ContextualSerializer
46+
47+
@Serializer(forClass = ParametrizedContextual::class)
48+
object ParametrizedContextualSerializer
49+
50+
@Serializable
51+
class ContextualHolder(@Contextual val contextual: ContextualType)
52+
53+
@Test
54+
fun testCompiled() = noJsLegacy {
55+
assertSame<KSerializer<*>>(Object.serializer(), serializer(Object::class, emptyList(), false))
56+
assertSame<KSerializer<*>>(SealedParent.serializer(), serializer(SealedParent::class, emptyList(), false))
57+
assertSame<KSerializer<*>>(
58+
SealedParent.Child.serializer(),
59+
serializer(SealedParent.Child::class, emptyList(), false)
60+
)
61+
62+
assertSame<KSerializer<*>>(Abstract.serializer(), serializer(Abstract::class, emptyList(), false))
63+
assertSame<KSerializer<*>>(SerializableEnum.serializer(), serializer(SerializableEnum::class, emptyList(), false))
64+
}
65+
66+
@Test
67+
fun testBuiltIn() {
68+
assertSame<KSerializer<*>>(Int.serializer(), serializer(Int::class, emptyList(), false))
69+
}
70+
71+
@Test
72+
fun testCustom() {
73+
val m = SerializersModule { }
74+
assertSame<KSerializer<*>>(CustomSerializer, m.serializer(WithCustomSerializer::class, emptyList(), false))
75+
}
76+
77+
@Test
78+
fun testParametrized() {
79+
val serializer = serializer(Parametrized::class, listOf(Int.serializer()), false)
80+
assertEquals<KClass<*>>(Parametrized.serializer(Int.serializer())::class, serializer::class)
81+
assertEquals(PrimitiveKind.INT, serializer.descriptor.getElementDescriptor(0).kind)
82+
83+
val mapSerializer = serializer(Map::class, listOf(String.serializer(), Int.serializer()), false)
84+
assertIs<MapLikeSerializer<*, *, *, *>>(mapSerializer)
85+
assertEquals(PrimitiveKind.STRING, mapSerializer.descriptor.getElementDescriptor(0).kind)
86+
assertEquals(PrimitiveKind.INT, mapSerializer.descriptor.getElementDescriptor(1).kind)
87+
}
88+
89+
@Test
90+
fun testUnsupportedArray() {
91+
assertFails {
92+
serializer(Array::class, listOf(Int.serializer()), false)
93+
}
94+
}
95+
96+
@Suppress("UNCHECKED_CAST")
97+
@Test
98+
fun testContextual() {
99+
val m = SerializersModule {
100+
contextual<ContextualType>(ContextualSerializer)
101+
contextual<ParametrizedContextual<*>>(ParametrizedContextualSerializer as KSerializer<ParametrizedContextual<*>>)
102+
contextual(ContextualGenericsTest.ThirdPartyBox::class) { args -> ContextualGenericsTest.ThirdPartyBoxSerializer(args[0]) }
103+
}
104+
105+
val contextualSerializer = m.serializer(ContextualType::class, emptyList(), false)
106+
assertSame<KSerializer<*>>(ContextualSerializer, contextualSerializer)
107+
108+
val boxSerializer = m.serializer(ContextualGenericsTest.ThirdPartyBox::class, listOf(Int.serializer()), false)
109+
assertIs<ContextualGenericsTest.ThirdPartyBoxSerializer<Int>>(boxSerializer)
110+
assertEquals(PrimitiveKind.INT, boxSerializer.descriptor.getElementDescriptor(0).kind)
111+
112+
val parametrizedSerializer = m.serializer(ParametrizedContextual::class, listOf(Int.serializer()), false)
113+
assertSame<KSerializer<*>>(ParametrizedContextualSerializer, parametrizedSerializer)
114+
115+
val holderSerializer = m.serializer(ContextualHolder::class, emptyList(), false)
116+
assertSame<KSerializer<*>>(ContextualHolder.serializer(), holderSerializer)
117+
}
118+
119+
}
120+

core/jvmTest/src/kotlinx/serialization/CachingTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class CachingTest {
3434
val cache = createParametrizedCache { clazz, types ->
3535
factoryCalled += 1
3636
val serializers = EmptySerializersModule().serializersForParameters(types, true)!!
37-
clazz.parametrizedSerializerOrNull(types, serializers)
37+
clazz.parametrizedSerializerOrNull(serializers) { types[0].classifier }
3838
}
3939

4040
repeat(10) {

0 commit comments

Comments
 (0)