Skip to content

Commit

Permalink
Function to retrieve KSerializer by KClass and type arguments seriali…
Browse files Browse the repository at this point in the history
…zers (#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>
  • Loading branch information
shanshin and sandwwraith authored May 10, 2023
1 parent 5a8795a commit a27e86f
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 13 deletions.
2 changes: 2 additions & 0 deletions core/api/kotlinx-serialization-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,10 @@ public final class kotlinx/serialization/SerializersKt {
public static final fun noCompiledSerializer (Lkotlinx/serialization/modules/SerializersModule;Lkotlin/reflect/KClass;[Lkotlinx/serialization/KSerializer;)Lkotlinx/serialization/KSerializer;
public static final fun serializer (Ljava/lang/reflect/Type;)Lkotlinx/serialization/KSerializer;
public static final fun serializer (Lkotlin/reflect/KClass;)Lkotlinx/serialization/KSerializer;
public static final fun serializer (Lkotlin/reflect/KClass;Ljava/util/List;Z)Lkotlinx/serialization/KSerializer;
public static final fun serializer (Lkotlin/reflect/KType;)Lkotlinx/serialization/KSerializer;
public static final fun serializer (Lkotlinx/serialization/modules/SerializersModule;Ljava/lang/reflect/Type;)Lkotlinx/serialization/KSerializer;
public static final fun serializer (Lkotlinx/serialization/modules/SerializersModule;Lkotlin/reflect/KClass;Ljava/util/List;Z)Lkotlinx/serialization/KSerializer;
public static final fun serializer (Lkotlinx/serialization/modules/SerializersModule;Lkotlin/reflect/KType;)Lkotlinx/serialization/KSerializer;
public static final fun serializerOrNull (Ljava/lang/reflect/Type;)Lkotlinx/serialization/KSerializer;
public static final fun serializerOrNull (Lkotlin/reflect/KClass;)Lkotlinx/serialization/KSerializer;
Expand Down
8 changes: 8 additions & 0 deletions core/build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile

/*
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
Expand Down Expand Up @@ -63,4 +65,10 @@ tasks.withType(Jar).named(kotlin.jvm().artifactsTaskName) {
}
}

tasks.withType(Kotlin2JsCompile.class).configureEach {
if (it.name == "compileTestKotlinJsLegacy") {
it.exclude("**/SerializersModuleTest.kt")
}
}

Java9Modularity.configureJava9ModuleInfo(project)
103 changes: 93 additions & 10 deletions core/commonMain/src/kotlinx/serialization/Serializers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,31 @@ public inline fun <reified T> SerializersModule.serializer(): KSerializer<T> {
*/
public fun serializer(type: KType): KSerializer<Any?> = EmptySerializersModule().serializer(type)


/**
* Retrieves serializer for the given [kClass].
* This method uses platform-specific reflection available.
*
* If [kClass] is a parametrized type then it is necessary to pass serializers for generic parameters in the [typeArgumentsSerializers].
* The nullability of returned serializer is specified using the [isNullable].
*
* Note that it is impossible to create an array serializer with this method,
* as array serializer needs additional information: type token for an element type.
* To create array serializer, use overload with [KType] or [ArraySerializer] directly.
*
* Caching on JVM platform is disabled for this function, so it may work slower than an overload with [KType].
*
* @throws SerializationException if serializer cannot be created (provided [kClass] or its type argument is not serializable)
* @throws SerializationException if [kClass] is a `kotlin.Array`
* @throws SerializationException if size of [typeArgumentsSerializers] does not match the expected generic parameters count
*/
@ExperimentalSerializationApi
public fun serializer(
kClass: KClass<*>,
typeArgumentsSerializers: List<KSerializer<*>>,
isNullable: Boolean
): KSerializer<Any?> = EmptySerializersModule().serializer(kClass, typeArgumentsSerializers, isNullable)

/**
* Creates a serializer for the given [type] if possible.
* [type] argument is usually obtained with [typeOf] method.
Expand Down Expand Up @@ -108,6 +133,34 @@ public fun SerializersModule.serializer(type: KType): KSerializer<Any?> =
serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = true) ?: type.kclass()
.platformSpecificSerializerNotRegistered()


/**
* Retrieves serializer for the given [kClass] and,
* if [kClass] is not serializable, fallbacks to [contextual][SerializersModule.getContextual] lookup.
* This method uses platform-specific reflection available.
*
* If [kClass] is a parametrized type then it is necessary to pass serializers for generic parameters in the [typeArgumentsSerializers].
* The nullability of returned serializer is specified using the [isNullable].
*
* Note that it is impossible to create an array serializer with this method,
* as array serializer needs additional information: type token for an element type.
* To create array serializer, use overload with [KType] or [ArraySerializer] directly.
*
* Caching on JVM platform is disabled for this function, so it may work slower than an overload with [KType].
*
* @throws SerializationException if serializer cannot be created (provided [kClass] or its type argument is not serializable and is not registered in [this] module)
* @throws SerializationException if [kClass] is a `kotlin.Array`
* @throws SerializationException if size of [typeArgumentsSerializers] does not match the expected generic parameters count
*/
@ExperimentalSerializationApi
public fun SerializersModule.serializer(
kClass: KClass<*>,
typeArgumentsSerializers: List<KSerializer<*>>,
isNullable: Boolean
): KSerializer<Any?> =
serializerByKClassImpl(kClass as KClass<Any>, typeArgumentsSerializers as List<KSerializer<Any?>>, isNullable)
?: kClass.platformSpecificSerializerNotRegistered()

/**
* Retrieves default serializer for the given [type] and,
* if [type] is not serializable, fallbacks to [contextual][SerializersModule.getContextual] lookup.
Expand Down Expand Up @@ -156,14 +209,39 @@ private fun SerializersModule.serializerByKTypeImpl(
} else {
val serializers = serializersForParameters(typeArguments, failOnMissingTypeArgSerializer) ?: return null
// first, we look among the built-in serializers, because the parameter could be contextual
rootClass.parametrizedSerializerOrNull(typeArguments, serializers) ?: getContextual(
rootClass,
serializers
)
rootClass.parametrizedSerializerOrNull(serializers) { typeArguments[0].classifier }
?: getContextual(
rootClass,
serializers
)
}
return contextualSerializer?.cast<Any>()?.nullable(isNullable)
}

@OptIn(ExperimentalSerializationApi::class)
private fun SerializersModule.serializerByKClassImpl(
rootClass: KClass<Any>,
typeArgumentsSerializers: List<KSerializer<Any?>>,
isNullable: Boolean
): KSerializer<Any?>? {
val serializer = if (typeArgumentsSerializers.isEmpty()) {
rootClass.serializerOrNull() ?: getContextual(rootClass)
} else {
try {
rootClass.parametrizedSerializerOrNull(typeArgumentsSerializers) {
throw SerializationException("It is not possible to retrieve an array serializer using KClass alone, use KType instead or ArraySerializer factory")
} ?: getContextual(
rootClass,
typeArgumentsSerializers
)
} catch (e: IndexOutOfBoundsException) {
throw SerializationException("Unable to retrieve a serializer, the number of passed type serializers differs from the actual number of generic parameters", e)
}
}

return serializer?.cast<Any>()?.nullable(isNullable)
}

/**
* Returns null only if `failOnMissingTypeArgSerializer == false` and at least one parameter serializer not found.
*/
Expand Down Expand Up @@ -230,11 +308,11 @@ public fun <T : Any> KClass<T>.serializerOrNull(): KSerializer<T>? =
compiledSerializerImpl() ?: builtinSerializerOrNull()

internal fun KClass<Any>.parametrizedSerializerOrNull(
types: List<KType>,
serializers: List<KSerializer<Any?>>
serializers: List<KSerializer<Any?>>,
elementClassifierIfArray: () -> KClassifier?
): KSerializer<out Any>? {
// builtin first because some standard parametrized interfaces (e.g. Map) must use builtin serializer but not polymorphic
return builtinParametrizedSerializer(types, serializers) ?: compiledParametrizedSerializer(serializers)
return builtinParametrizedSerializer(serializers, elementClassifierIfArray) ?: compiledParametrizedSerializer(serializers)
}


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

@OptIn(ExperimentalSerializationApi::class)
private fun KClass<Any>.builtinParametrizedSerializer(
typeArguments: List<KType>,
serializers: List<KSerializer<Any?>>,
elementClassifierIfArray: () -> KClassifier?
): KSerializer<out Any>? {
return when (this) {
Collection::class, List::class, MutableList::class, ArrayList::class -> ArrayListSerializer(serializers[0])
Expand All @@ -256,12 +334,13 @@ private fun KClass<Any>.builtinParametrizedSerializer(
serializers[0],
serializers[1]
)

Map.Entry::class -> MapEntrySerializer(serializers[0], serializers[1])
Pair::class -> PairSerializer(serializers[0], serializers[1])
Triple::class -> TripleSerializer(serializers[0], serializers[1], serializers[2])
else -> {
if (isReferenceArray(this)) {
ArraySerializer(typeArguments[0].classifier as KClass<Any>, serializers[0])
ArraySerializer(elementClassifierIfArray() as KClass<Any>, serializers[0])
} else {
null
}
Expand Down Expand Up @@ -297,6 +376,10 @@ internal fun noCompiledSerializer(module: SerializersModule, kClass: KClass<*>):
@OptIn(ExperimentalSerializationApi::class)
@Suppress("unused")
@PublishedApi
internal fun noCompiledSerializer(module: SerializersModule, kClass: KClass<*>, argSerializers: Array<KSerializer<*>>): KSerializer<*> {
internal fun noCompiledSerializer(
module: SerializersModule,
kClass: KClass<*>,
argSerializers: Array<KSerializer<*>>
): KSerializer<*> {
return module.getContextual(kClass, argSerializers.asList()) ?: kClass.serializerNotRegistered()
}
4 changes: 2 additions & 2 deletions core/commonMain/src/kotlinx/serialization/SerializersCache.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ private val SERIALIZERS_CACHE_NULLABLE = createCache<Any?> { it.serializerOrNull
@ThreadLocal
private val PARAMETRIZED_SERIALIZERS_CACHE = createParametrizedCache { clazz, types ->
val serializers = EmptySerializersModule().serializersForParameters(types, true)!!
clazz.parametrizedSerializerOrNull(types, serializers)
clazz.parametrizedSerializerOrNull(serializers) { types[0].classifier }
}

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

/**
Expand Down
120 changes: 120 additions & 0 deletions core/commonTest/src/kotlinx/serialization/SerializersModuleTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization

import kotlinx.serialization.builtins.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.internal.*
import kotlinx.serialization.modules.*
import kotlinx.serialization.test.*
import kotlin.reflect.*
import kotlin.test.*

class SerializersModuleTest {
@Serializable
object Object

@Serializable
sealed class SealedParent {
@Serializable
data class Child(val i: Int) : SealedParent()
}

@Serializable
abstract class Abstract

@Serializable
enum class SerializableEnum { A, B }

@Serializable(CustomSerializer::class)
class WithCustomSerializer(val i: Int)

@Serializer(forClass = WithCustomSerializer::class)
object CustomSerializer

@Serializable
class Parametrized<T : Any>(val a: T)

class ContextualType(val i: Int)

class ParametrizedContextual<T : Any>(val a: T)

@Serializer(forClass = ContextualType::class)
object ContextualSerializer

@Serializer(forClass = ParametrizedContextual::class)
object ParametrizedContextualSerializer

@Serializable
class ContextualHolder(@Contextual val contextual: ContextualType)

@Test
fun testCompiled() = noJsLegacy {
assertSame<KSerializer<*>>(Object.serializer(), serializer(Object::class, emptyList(), false))
assertSame<KSerializer<*>>(SealedParent.serializer(), serializer(SealedParent::class, emptyList(), false))
assertSame<KSerializer<*>>(
SealedParent.Child.serializer(),
serializer(SealedParent.Child::class, emptyList(), false)
)

assertSame<KSerializer<*>>(Abstract.serializer(), serializer(Abstract::class, emptyList(), false))
assertSame<KSerializer<*>>(SerializableEnum.serializer(), serializer(SerializableEnum::class, emptyList(), false))
}

@Test
fun testBuiltIn() {
assertSame<KSerializer<*>>(Int.serializer(), serializer(Int::class, emptyList(), false))
}

@Test
fun testCustom() {
val m = SerializersModule { }
assertSame<KSerializer<*>>(CustomSerializer, m.serializer(WithCustomSerializer::class, emptyList(), false))
}

@Test
fun testParametrized() {
val serializer = serializer(Parametrized::class, listOf(Int.serializer()), false)
assertEquals<KClass<*>>(Parametrized.serializer(Int.serializer())::class, serializer::class)
assertEquals(PrimitiveKind.INT, serializer.descriptor.getElementDescriptor(0).kind)

val mapSerializer = serializer(Map::class, listOf(String.serializer(), Int.serializer()), false)
assertIs<MapLikeSerializer<*, *, *, *>>(mapSerializer)
assertEquals(PrimitiveKind.STRING, mapSerializer.descriptor.getElementDescriptor(0).kind)
assertEquals(PrimitiveKind.INT, mapSerializer.descriptor.getElementDescriptor(1).kind)
}

@Test
fun testUnsupportedArray() {
assertFails {
serializer(Array::class, listOf(Int.serializer()), false)
}
}

@Suppress("UNCHECKED_CAST")
@Test
fun testContextual() {
val m = SerializersModule {
contextual<ContextualType>(ContextualSerializer)
contextual<ParametrizedContextual<*>>(ParametrizedContextualSerializer as KSerializer<ParametrizedContextual<*>>)
contextual(ContextualGenericsTest.ThirdPartyBox::class) { args -> ContextualGenericsTest.ThirdPartyBoxSerializer(args[0]) }
}

val contextualSerializer = m.serializer(ContextualType::class, emptyList(), false)
assertSame<KSerializer<*>>(ContextualSerializer, contextualSerializer)

val boxSerializer = m.serializer(ContextualGenericsTest.ThirdPartyBox::class, listOf(Int.serializer()), false)
assertIs<ContextualGenericsTest.ThirdPartyBoxSerializer<Int>>(boxSerializer)
assertEquals(PrimitiveKind.INT, boxSerializer.descriptor.getElementDescriptor(0).kind)

val parametrizedSerializer = m.serializer(ParametrizedContextual::class, listOf(Int.serializer()), false)
assertSame<KSerializer<*>>(ParametrizedContextualSerializer, parametrizedSerializer)

val holderSerializer = m.serializer(ContextualHolder::class, emptyList(), false)
assertSame<KSerializer<*>>(ContextualHolder.serializer(), holderSerializer)
}

}

2 changes: 1 addition & 1 deletion core/jvmTest/src/kotlinx/serialization/CachingTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class CachingTest {
val cache = createParametrizedCache { clazz, types ->
factoryCalled += 1
val serializers = EmptySerializersModule().serializersForParameters(types, true)!!
clazz.parametrizedSerializerOrNull(types, serializers)
clazz.parametrizedSerializerOrNull(serializers) { types[0].classifier }
}

repeat(10) {
Expand Down

0 comments on commit a27e86f

Please sign in to comment.