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

Support contextual serialization of generic classes #1416

Merged
merged 7 commits into from
Apr 20, 2021
Merged
Show file tree
Hide file tree
Changes from 6 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
10 changes: 9 additions & 1 deletion core/api/kotlinx-serialization-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -1156,14 +1156,17 @@ public final class kotlinx/serialization/modules/PolymorphicModuleBuilder {

public abstract class kotlinx/serialization/modules/SerializersModule {
public abstract fun dumpTo (Lkotlinx/serialization/modules/SerializersModuleCollector;)V
public abstract fun getContextual (Lkotlin/reflect/KClass;)Lkotlinx/serialization/KSerializer;
public final synthetic fun getContextual (Lkotlin/reflect/KClass;)Lkotlinx/serialization/KSerializer;
public abstract fun getContextual (Lkotlin/reflect/KClass;Ljava/util/List;)Lkotlinx/serialization/KSerializer;
public static synthetic fun getContextual$default (Lkotlinx/serialization/modules/SerializersModule;Lkotlin/reflect/KClass;Ljava/util/List;ILjava/lang/Object;)Lkotlinx/serialization/KSerializer;
public abstract fun getPolymorphic (Lkotlin/reflect/KClass;Ljava/lang/Object;)Lkotlinx/serialization/SerializationStrategy;
public abstract fun getPolymorphic (Lkotlin/reflect/KClass;Ljava/lang/String;)Lkotlinx/serialization/DeserializationStrategy;
}

public final class kotlinx/serialization/modules/SerializersModuleBuilder : kotlinx/serialization/modules/SerializersModuleCollector {
public fun <init> ()V
public final fun build ()Lkotlinx/serialization/modules/SerializersModule;
public fun contextual (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V
public fun contextual (Lkotlin/reflect/KClass;Lkotlinx/serialization/KSerializer;)V
public final fun include (Lkotlinx/serialization/modules/SerializersModule;)V
public fun polymorphic (Lkotlin/reflect/KClass;Lkotlin/reflect/KClass;Lkotlinx/serialization/KSerializer;)V
Expand All @@ -1178,11 +1181,16 @@ public final class kotlinx/serialization/modules/SerializersModuleBuildersKt {
}

public abstract interface class kotlinx/serialization/modules/SerializersModuleCollector {
public abstract fun contextual (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V
public abstract fun contextual (Lkotlin/reflect/KClass;Lkotlinx/serialization/KSerializer;)V
public abstract fun polymorphic (Lkotlin/reflect/KClass;Lkotlin/reflect/KClass;Lkotlinx/serialization/KSerializer;)V
public abstract fun polymorphicDefault (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V
}

public final class kotlinx/serialization/modules/SerializersModuleCollector$DefaultImpls {
public static fun contextual (Lkotlinx/serialization/modules/SerializersModuleCollector;Lkotlin/reflect/KClass;Lkotlinx/serialization/KSerializer;)V
}

public final class kotlinx/serialization/modules/SerializersModuleKt {
public static final fun getEmptySerializersModule ()Lkotlinx/serialization/modules/SerializersModule;
public static final fun overwriteWith (Lkotlinx/serialization/modules/SerializersModule;Lkotlinx/serialization/modules/SerializersModule;)Lkotlinx/serialization/modules/SerializersModule;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization
Expand Down Expand Up @@ -42,13 +42,16 @@ import kotlin.reflect.*
public class ContextualSerializer<T : Any>(
private val serializableClass: KClass<T>,
private val fallbackSerializer: KSerializer<T>?,
private val typeParametersSerializers: Array<KSerializer<*>>
typeArgumentsSerializers: Array<KSerializer<*>>
) : KSerializer<T> {

private val typeArgumentsSerializers: List<KSerializer<*>> = typeArgumentsSerializers.asList()

private fun serializer(serializersModule: SerializersModule): KSerializer<T> =
serializersModule.getContextual(serializableClass) ?: fallbackSerializer ?: serializableClass.serializerNotRegistered()
serializersModule.getContextual(serializableClass, typeArgumentsSerializers) ?: fallbackSerializer ?: serializableClass.serializerNotRegistered()

// Used from auto-generated code
// Used from the old plugins
@Suppress("unused")
public constructor(serializableClass: KClass<T>) : this(serializableClass, null, EMPTY_SERIALIZER_ARRAY)

public override val descriptor: SerialDescriptor =
Expand Down
11 changes: 6 additions & 5 deletions core/commonMain/src/kotlinx/serialization/Serializers.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
@file:Suppress("DEPRECATION_ERROR", "UNCHECKED_CAST")
@file:JvmMultifileClass
Expand Down Expand Up @@ -114,15 +114,16 @@ private fun SerializersModule.builtinSerializer(
if (isReferenceArray(rootClass)) {
return ArraySerializer<Any, Any?>(typeArguments[0].classifier as KClass<Any>, serializers[0]).cast()
}
rootClass.constructSerializerForGivenTypeArgs(*serializers.toTypedArray())
?: reflectiveOrContextual(rootClass)
val args = serializers.toTypedArray()
rootClass.constructSerializerForGivenTypeArgs(*args)
?: reflectiveOrContextual(rootClass, serializers)
}
}
}

@OptIn(ExperimentalSerializationApi::class)
internal fun <T : Any> SerializersModule.reflectiveOrContextual(kClass: KClass<T>): KSerializer<T>? {
return kClass.serializerOrNull() ?: getContextual(kClass)
internal fun <T : Any> SerializersModule.reflectiveOrContextual(kClass: KClass<T>, typeArgumentsSerializers: List<KSerializer<Any?>>): KSerializer<T>? {
return kClass.serializerOrNull() ?: getContextual(kClass, typeArgumentsSerializers)
}


Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization.modules
Expand All @@ -23,12 +23,28 @@ import kotlin.reflect.*
*/
public sealed class SerializersModule {

@ExperimentalSerializationApi
@Deprecated(
"Deprecated in favor of overload with default parameter",
ReplaceWith("getContextual(kclass)"),
DeprecationLevel.HIDDEN
)
sandwwraith marked this conversation as resolved.
Show resolved Hide resolved
public fun <T : Any> getContextual(kclass: KClass<T>): KSerializer<T>? =
sandwwraith marked this conversation as resolved.
Show resolved Hide resolved
getContextual(kclass, emptyList())

/**
* Returns a contextual serializer associated with a given [kclass].
* This method is used in context-sensitive operations on a property marked with [Contextual] by a [ContextualSerializer]
* Returns a contextual serializer associated with a given [kClass].
* If given class has generic parameters and module has provider for [kClass],
* [typeArgumentsSerializers] are used to create serializer.
* This method is used in context-sensitive operations on a property marked with [Contextual] by a [ContextualSerializer].
*
* @see SerializersModuleBuilder.contextual
*/
@ExperimentalSerializationApi
public abstract fun <T : Any> getContextual(kclass: KClass<T>): KSerializer<T>?
public abstract fun <T : Any> getContextual(
sandwwraith marked this conversation as resolved.
Show resolved Hide resolved
kClass: KClass<T>,
typeArgumentsSerializers: List<KSerializer<*>> = emptyList()
): KSerializer<T>?

/**
* Returns a polymorphic serializer registered for a class of the given [value] in the scope of [baseClass].
Expand Down Expand Up @@ -74,11 +90,19 @@ public operator fun SerializersModule.plus(other: SerializersModule): Serializer
* If serializer for some class presents in both modules, result module
* will contain serializer from [other] module.
*/
@OptIn(ExperimentalSerializationApi::class)
public infix fun SerializersModule.overwriteWith(other: SerializersModule): SerializersModule = SerializersModule {
include(this@overwriteWith)
other.dumpTo(object : SerializersModuleCollector {
override fun <T : Any> contextual(kClass: KClass<T>, serializer: KSerializer<T>) {
registerSerializer(kClass, serializer, allowOverwrite = true)
registerSerializer(kClass, ContextualProvider.Argless(serializer), allowOverwrite = true)
}

override fun <T : Any> contextual(
sandwwraith marked this conversation as resolved.
Show resolved Hide resolved
kClass: KClass<T>,
provider: (serializers: List<KSerializer<*>>) -> KSerializer<*>
) {
registerSerializer(kClass, ContextualProvider.WithTypeArguments(provider), allowOverwrite = true)
}

override fun <Base : Any, Sub : Base> polymorphic(
Expand All @@ -105,8 +129,9 @@ public infix fun SerializersModule.overwriteWith(other: SerializersModule): Seri
* which uses hash maps to store serializers associated with KClasses.
*/
@Suppress("UNCHECKED_CAST")
@OptIn(ExperimentalSerializationApi::class)
internal class SerialModuleImpl(
private val class2Serializer: Map<KClass<*>, KSerializer<*>>,
private val class2ContextualFactory: Map<KClass<*>, ContextualProvider>,
@JvmField val polyBase2Serializers: Map<KClass<*>, Map<KClass<*>, KSerializer<*>>>,
private val polyBase2NamedSerializers: Map<KClass<*>, Map<String, KSerializer<*>>>,
private val polyBase2DefaultProvider: Map<KClass<*>, PolymorphicProvider<*>>
Expand All @@ -125,15 +150,19 @@ internal class SerialModuleImpl(
return (polyBase2DefaultProvider[baseClass] as? PolymorphicProvider<T>)?.invoke(serializedClassName)
}

override fun <T : Any> getContextual(kclass: KClass<T>): KSerializer<T>? =
class2Serializer[kclass] as? KSerializer<T>
override fun <T : Any> getContextual(kClass: KClass<T>, typeArgumentsSerializers: List<KSerializer<*>>): KSerializer<T>? {
return (class2ContextualFactory[kClass]?.invoke(typeArgumentsSerializers)) as? KSerializer<T>?
}

override fun dumpTo(collector: SerializersModuleCollector) {
class2Serializer.forEach { (kclass, serial) ->
collector.contextual(
kclass as KClass<Any>,
serial.cast()
)
class2ContextualFactory.forEach { (kclass, serial) ->
when (serial) {
is ContextualProvider.Argless -> collector.contextual(
kclass as KClass<Any>,
serial.serializer as KSerializer<Any>
)
is ContextualProvider.WithTypeArguments -> collector.contextual(kclass, serial.provider)
}
}

polyBase2Serializers.forEach { (baseClass, classMap) ->
Expand All @@ -153,3 +182,30 @@ internal class SerialModuleImpl(
}

internal typealias PolymorphicProvider<Base> = (className: String?) -> DeserializationStrategy<out Base>?

/** This class is needed to support re-registering the same static (argless) serializers:
*
* ```
* val m1 = serializersModuleOf(A::class, A.serializer())
* val m2 = serializersModuleOf(A::class, A.serializer())
* val aggregate = m1 + m2 // should not throw
* ```
*/
internal sealed class ContextualProvider {
abstract operator fun invoke(typeArgumentsSerializers: List<KSerializer<*>>): KSerializer<*>

class Argless(val serializer: KSerializer<*>) : ContextualProvider() {
override fun invoke(typeArgumentsSerializers: List<KSerializer<*>>): KSerializer<*> = serializer

override fun equals(other: Any?): Boolean = other is Argless && other.serializer == this.serializer

override fun hashCode(): Int = serializer.hashCode()
}

class WithTypeArguments(val provider: (typeArgumentsSerializers: List<KSerializer<*>>) -> KSerializer<*>) :
ContextualProvider() {
override fun invoke(typeArgumentsSerializers: List<KSerializer<*>>): KSerializer<*> =
provider(typeArgumentsSerializers)
}

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/*
* Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
package kotlinx.serialization.modules

import kotlinx.serialization.*
import kotlinx.serialization.internal.*
import kotlin.jvm.*
import kotlin.reflect.*

Expand Down Expand Up @@ -37,18 +38,43 @@ public inline fun SerializersModule(builderAction: SerializersModuleBuilder.() -
*/
@OptIn(ExperimentalSerializationApi::class)
public class SerializersModuleBuilder @PublishedApi internal constructor() : SerializersModuleCollector {
private val class2Serializer: MutableMap<KClass<*>, KSerializer<*>> = hashMapOf()
private val class2ContextualProvider: MutableMap<KClass<*>, ContextualProvider> = hashMapOf()
private val polyBase2Serializers: MutableMap<KClass<*>, MutableMap<KClass<*>, KSerializer<*>>> = hashMapOf()
private val polyBase2NamedSerializers: MutableMap<KClass<*>, MutableMap<String, KSerializer<*>>> = hashMapOf()
private val polyBase2DefaultProvider: MutableMap<KClass<*>, PolymorphicProvider<*>> = hashMapOf()

/**
* Adds [serializer] associated with given [kClass] for contextual serialization.
* Throws [SerializationException] if a module already has serializer associated with a [kClass].
* If [kClass] has generic type parameters, consider registering provider instead.
*
* Throws [SerializationException] if a module already has serializer or provider associated with a [kClass].
* To overwrite an already registered serializer, [SerializersModule.overwriteWith] can be used.
*/
public override fun <T : Any> contextual(kClass: KClass<T>, serializer: KSerializer<T>): Unit =
registerSerializer(kClass, serializer)
registerSerializer(kClass, ContextualProvider.Argless(serializer))

/**
* Registers [provider] associated with given generic [kClass] for contextual serialization.
* When a serializer is requested from a module, provider is being called with type arguments serializers
* of the particular [kClass] usage.
*
* Example:
* ```
* class Holder(@Contextual val boxI: Box<Int>, @Contextual val boxS: Box<String>)
*
* val module = SerializersModule {
* // args[0] contains Int.serializer() or String.serializer(), depending on the property
* contextual(Box::class) { args -> BoxSerializer(args[0]) }
* }
* ```
*
* Throws [SerializationException] if a module already has provider or serializer associated with a [kClass].
* To overwrite an already registered serializer, [SerializersModule.overwriteWith] can be used.
*/
public override fun <T : Any> contextual(
sandwwraith marked this conversation as resolved.
Show resolved Hide resolved
kClass: KClass<T>,
provider: (typeArgumentsSerializers: List<KSerializer<*>>) -> KSerializer<*>
): Unit = registerSerializer(kClass, ContextualProvider.WithTypeArguments(provider))

/**
* Adds [serializer][actualSerializer] associated with given [actualClass] in the scope of [baseClass] for polymorphic serialization.
Expand Down Expand Up @@ -88,22 +114,19 @@ public class SerializersModuleBuilder @PublishedApi internal constructor() : Ser
@JvmName("registerSerializer") // Don't mangle method name for prettier stack traces
internal fun <T : Any> registerSerializer(
forClass: KClass<T>,
serializer: KSerializer<T>,
provider: ContextualProvider,
allowOverwrite: Boolean = false
) {
if (!allowOverwrite) {
val previous = class2Serializer[forClass]
if (previous != null && previous != serializer) {
// TODO when working on SD rework, provide a way to properly stringify serializer as its FQN
val currentName = serializer.descriptor.serialName
val previousName = previous.descriptor.serialName
val previous = class2ContextualProvider[forClass]
if (previous != null && previous != provider) {
// How can we provide meaningful name for WithTypeArgumentsProvider ?
throw SerializerAlreadyRegisteredException(
"Serializer for $forClass already registered in this module: $previous ($previousName), " +
"attempted to register $serializer ($currentName)"
"Contextual serializer or serializer provider for $forClass already registered in this module"
)
}
}
class2Serializer[forClass] = serializer
class2ContextualProvider[forClass] = provider
}

@JvmName("registerDefaultPolymorphicSerializer") // Don't mangle method name for prettier stack traces
Expand Down Expand Up @@ -165,7 +188,7 @@ public class SerializersModuleBuilder @PublishedApi internal constructor() : Ser

@PublishedApi
internal fun build(): SerializersModule =
SerialModuleImpl(class2Serializer, polyBase2Serializers, polyBase2NamedSerializers, polyBase2DefaultProvider)
SerialModuleImpl(class2ContextualProvider, polyBase2Serializers, polyBase2NamedSerializers, polyBase2DefaultProvider)
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

@file:Suppress("RedundantVisibilityModifier")
Expand All @@ -24,7 +24,17 @@ public interface SerializersModuleCollector {
/**
* Accept a serializer, associated with [kClass] for contextual serialization.
*/
public fun <T : Any> contextual(kClass: KClass<T>, serializer: KSerializer<T>)
public fun <T : Any> contextual(kClass: KClass<T>, serializer: KSerializer<T>): Unit =
contextual(kClass) { serializer }


/**
* Accept a provider, associated with generic [kClass] for contextual serialization.
*/
public fun <T : Any> contextual(
kClass: KClass<T>,
provider: (typeArgumentsSerializers: List<KSerializer<*>>) -> KSerializer<*>
)

/**
* Accept a serializer, associated with [actualClass] for polymorphic serialization.
Expand Down

This file was deleted.

Loading