Skip to content

Commit

Permalink
Support contextual serialization of generic classes (#1416)
Browse files Browse the repository at this point in the history
via special lambda provider in SerializersModule
in serializersModule.getContextual(..) and serializer<T>()

Fixes #1407
  • Loading branch information
sandwwraith authored Apr 20, 2021
1 parent 59d3216 commit c6025c9
Show file tree
Hide file tree
Showing 16 changed files with 317 additions and 107 deletions.
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
) // Was stable since 1.0.0, HIDDEN in 1.2.0 in a backwards-compatible manner
public fun <T : Any> getContextual(kclass: KClass<T>): KSerializer<T>? =
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(
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(
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(
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

0 comments on commit c6025c9

Please sign in to comment.