From a27e86f7aa71eeb482da99e32e8a131e2d484daa Mon Sep 17 00:00:00 2001 From: Sergey Shanshin Date: Wed, 10 May 2023 20:13:37 +0300 Subject: [PATCH] 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 --- core/api/kotlinx-serialization-core.api | 2 + core/build.gradle | 8 ++ .../src/kotlinx/serialization/Serializers.kt | 103 +++++++++++++-- .../kotlinx/serialization/SerializersCache.kt | 4 +- .../serialization/SerializersModuleTest.kt | 120 ++++++++++++++++++ .../src/kotlinx/serialization/CachingTest.kt | 2 +- 6 files changed, 226 insertions(+), 13 deletions(-) create mode 100644 core/commonTest/src/kotlinx/serialization/SerializersModuleTest.kt diff --git a/core/api/kotlinx-serialization-core.api b/core/api/kotlinx-serialization-core.api index e6d526adde..ea4b50e797 100644 --- a/core/api/kotlinx-serialization-core.api +++ b/core/api/kotlinx-serialization-core.api @@ -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; diff --git a/core/build.gradle b/core/build.gradle index 4b05ba6ca2..064b6d4cb5 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -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. */ @@ -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) diff --git a/core/commonMain/src/kotlinx/serialization/Serializers.kt b/core/commonMain/src/kotlinx/serialization/Serializers.kt index fb34224ee9..2880dd5899 100644 --- a/core/commonMain/src/kotlinx/serialization/Serializers.kt +++ b/core/commonMain/src/kotlinx/serialization/Serializers.kt @@ -71,6 +71,31 @@ public inline fun SerializersModule.serializer(): KSerializer { */ public fun serializer(type: KType): KSerializer = 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>, + isNullable: Boolean +): KSerializer = EmptySerializersModule().serializer(kClass, typeArgumentsSerializers, isNullable) + /** * Creates a serializer for the given [type] if possible. * [type] argument is usually obtained with [typeOf] method. @@ -108,6 +133,34 @@ public fun SerializersModule.serializer(type: KType): KSerializer = 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>, + isNullable: Boolean +): KSerializer = + serializerByKClassImpl(kClass as KClass, typeArgumentsSerializers as List>, isNullable) + ?: kClass.platformSpecificSerializerNotRegistered() + /** * Retrieves default serializer for the given [type] and, * if [type] is not serializable, fallbacks to [contextual][SerializersModule.getContextual] lookup. @@ -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()?.nullable(isNullable) } +@OptIn(ExperimentalSerializationApi::class) +private fun SerializersModule.serializerByKClassImpl( + rootClass: KClass, + typeArgumentsSerializers: List>, + isNullable: Boolean +): KSerializer? { + 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()?.nullable(isNullable) +} + /** * Returns null only if `failOnMissingTypeArgSerializer == false` and at least one parameter serializer not found. */ @@ -230,11 +308,11 @@ public fun KClass.serializerOrNull(): KSerializer? = compiledSerializerImpl() ?: builtinSerializerOrNull() internal fun KClass.parametrizedSerializerOrNull( - types: List, - serializers: List> + serializers: List>, + elementClassifierIfArray: () -> KClassifier? ): KSerializer? { // 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) } @@ -244,8 +322,8 @@ private fun KClass.compiledParametrizedSerializer(serializers: List.builtinParametrizedSerializer( - typeArguments: List, serializers: List>, + elementClassifierIfArray: () -> KClassifier? ): KSerializer? { return when (this) { Collection::class, List::class, MutableList::class, ArrayList::class -> ArrayListSerializer(serializers[0]) @@ -256,12 +334,13 @@ private fun KClass.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, serializers[0]) + ArraySerializer(elementClassifierIfArray() as KClass, serializers[0]) } else { null } @@ -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<*> { +internal fun noCompiledSerializer( + module: SerializersModule, + kClass: KClass<*>, + argSerializers: Array> +): KSerializer<*> { return module.getContextual(kClass, argSerializers.asList()) ?: kClass.serializerNotRegistered() } diff --git a/core/commonMain/src/kotlinx/serialization/SerializersCache.kt b/core/commonMain/src/kotlinx/serialization/SerializersCache.kt index b1481884d6..cc86e4358d 100644 --- a/core/commonMain/src/kotlinx/serialization/SerializersCache.kt +++ b/core/commonMain/src/kotlinx/serialization/SerializersCache.kt @@ -32,7 +32,7 @@ private val SERIALIZERS_CACHE_NULLABLE = createCache { 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 } } /** @@ -41,7 +41,7 @@ private val PARAMETRIZED_SERIALIZERS_CACHE = createParametrizedCache { clazz, ty @ThreadLocal private val PARAMETRIZED_SERIALIZERS_CACHE_NULLABLE = createParametrizedCache { clazz, types -> val serializers = EmptySerializersModule().serializersForParameters(types, true)!! - clazz.parametrizedSerializerOrNull(types, serializers)?.nullable?.cast() + clazz.parametrizedSerializerOrNull(serializers) { types[0].classifier }?.nullable?.cast() } /** diff --git a/core/commonTest/src/kotlinx/serialization/SerializersModuleTest.kt b/core/commonTest/src/kotlinx/serialization/SerializersModuleTest.kt new file mode 100644 index 0000000000..75da603348 --- /dev/null +++ b/core/commonTest/src/kotlinx/serialization/SerializersModuleTest.kt @@ -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(val a: T) + + class ContextualType(val i: Int) + + class ParametrizedContextual(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>(Object.serializer(), serializer(Object::class, emptyList(), false)) + assertSame>(SealedParent.serializer(), serializer(SealedParent::class, emptyList(), false)) + assertSame>( + SealedParent.Child.serializer(), + serializer(SealedParent.Child::class, emptyList(), false) + ) + + assertSame>(Abstract.serializer(), serializer(Abstract::class, emptyList(), false)) + assertSame>(SerializableEnum.serializer(), serializer(SerializableEnum::class, emptyList(), false)) + } + + @Test + fun testBuiltIn() { + assertSame>(Int.serializer(), serializer(Int::class, emptyList(), false)) + } + + @Test + fun testCustom() { + val m = SerializersModule { } + assertSame>(CustomSerializer, m.serializer(WithCustomSerializer::class, emptyList(), false)) + } + + @Test + fun testParametrized() { + val serializer = serializer(Parametrized::class, listOf(Int.serializer()), false) + assertEquals>(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>(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(ContextualSerializer) + contextual>(ParametrizedContextualSerializer as KSerializer>) + contextual(ContextualGenericsTest.ThirdPartyBox::class) { args -> ContextualGenericsTest.ThirdPartyBoxSerializer(args[0]) } + } + + val contextualSerializer = m.serializer(ContextualType::class, emptyList(), false) + assertSame>(ContextualSerializer, contextualSerializer) + + val boxSerializer = m.serializer(ContextualGenericsTest.ThirdPartyBox::class, listOf(Int.serializer()), false) + assertIs>(boxSerializer) + assertEquals(PrimitiveKind.INT, boxSerializer.descriptor.getElementDescriptor(0).kind) + + val parametrizedSerializer = m.serializer(ParametrizedContextual::class, listOf(Int.serializer()), false) + assertSame>(ParametrizedContextualSerializer, parametrizedSerializer) + + val holderSerializer = m.serializer(ContextualHolder::class, emptyList(), false) + assertSame>(ContextualHolder.serializer(), holderSerializer) + } + +} + diff --git a/core/jvmTest/src/kotlinx/serialization/CachingTest.kt b/core/jvmTest/src/kotlinx/serialization/CachingTest.kt index f3e303cd08..b146c92053 100644 --- a/core/jvmTest/src/kotlinx/serialization/CachingTest.kt +++ b/core/jvmTest/src/kotlinx/serialization/CachingTest.kt @@ -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) {