Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented obtainment of KSerializer by KClass in SerializersModule #2291

Merged
merged 10 commits into from
May 10, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
98 changes: 89 additions & 9 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 an array class
* @throws IndexOutOfBoundsException 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 IndexOutOfBoundsException if size of [typeArgumentsSerializers] does not match the expected generic parameters count
*/
@ExperimentalSerializationApi
public fun <T : Any> SerializersModule.serializer(
shanshin marked this conversation as resolved.
Show resolved Hide resolved
kClass: KClass<T>,
typeArgumentsSerializers: List<KSerializer<*>>,
isNullable: Boolean
): KSerializer<Any?> =
shanshin marked this conversation as resolved.
Show resolved Hide resolved
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,12 +209,34 @@ 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.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 {
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,
serializers
typeArgumentsSerializers
)
}
return contextualSerializer?.cast<Any>()?.nullable(isNullable)

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

/**
Expand Down Expand Up @@ -230,11 +305,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 +319,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 +331,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 +373,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
126 changes: 126 additions & 0 deletions core/commonTest/src/kotlinx/serialization/SerializersModuleTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

@file:UseContextualSerialization(SerializersModuleTest.FileContextualType::class)

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

class FileContextualType(val i: Int)

@Serializer(forClass = FileContextualType::class)
object FileContextualSerializer

@Serializable
class ContextualHolder(@Contextual val contextual: ContextualType, val fileContextual: FileContextualType)

@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() {
shanshin marked this conversation as resolved.
Show resolved Hide resolved
val m = SerializersModule {
contextual<FileContextualType>(FileContextualSerializer)
shanshin marked this conversation as resolved.
Show resolved Hide resolved
contextual<ContextualType>(ContextualSerializer)
contextual<ParametrizedContextual<*>>(ParametrizedContextualSerializer as KSerializer<ParametrizedContextual<*>>)
shanshin marked this conversation as resolved.
Show resolved Hide resolved
}

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

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

val fileContextualSerializer = m.serializer(FileContextualType::class, emptyList(), false)
assertSame<KSerializer<*>>(FileContextualSerializer, fileContextualSerializer)

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