Skip to content

Commit b811fa3

Browse files
authored
Change priority for PolymorphicSerializer inside serializer() function (#2565)
Now, we look up serializers for non-sealed interfaces in SerializersModule first, and only then unconditionally return PolymorphicSerializer. This is done because with old behavior, it was impossible to override interface serializer with context, as interfaces were treated as always having PolymorphicSerializer. This is a behavioral change, although the impact should be minimal, as for most use cases (without modules), the serializer will be the same, and special workarounds are performed so the cache will also work as expected. Note that KClass.serializer() function will no longer return PolymorphicSerializer for interfaces at all and return null instead. It is OK, because this function is marked with @InternalSerializationApi, and we can change its behavior. Intrinsics in the plugin with changed functionality are merged in 2.0.0-RC. Fixes #2060
1 parent 1f7372a commit b811fa3

File tree

16 files changed

+414
-44
lines changed

16 files changed

+414
-44
lines changed

core/api/kotlinx-serialization-core.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ public abstract interface annotation class kotlinx/serialization/Serializer : ja
123123
}
124124

125125
public final class kotlinx/serialization/SerializersKt {
126+
public static final fun moduleThenPolymorphic (Lkotlinx/serialization/modules/SerializersModule;Lkotlin/reflect/KClass;)Lkotlinx/serialization/KSerializer;
127+
public static final fun moduleThenPolymorphic (Lkotlinx/serialization/modules/SerializersModule;Lkotlin/reflect/KClass;[Lkotlinx/serialization/KSerializer;)Lkotlinx/serialization/KSerializer;
126128
public static final fun noCompiledSerializer (Ljava/lang/String;)Lkotlinx/serialization/KSerializer;
127129
public static final fun noCompiledSerializer (Lkotlinx/serialization/modules/SerializersModule;Lkotlin/reflect/KClass;)Lkotlinx/serialization/KSerializer;
128130
public static final fun noCompiledSerializer (Lkotlinx/serialization/modules/SerializersModule;Lkotlin/reflect/KClass;[Lkotlinx/serialization/KSerializer;)Lkotlinx/serialization/KSerializer;

core/commonMain/src/kotlinx/serialization/Serializers.kt

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -189,24 +189,45 @@ private fun SerializersModule.serializerByKTypeImpl(
189189
val isNullable = type.isMarkedNullable
190190
val typeArguments = type.arguments.map(KTypeProjection::typeOrThrow)
191191

192-
val cachedSerializer = if (typeArguments.isEmpty()) {
193-
findCachedSerializer(rootClass, isNullable)
192+
val cachedSerializer = if (typeArguments.isEmpty()) {
193+
if (rootClass.isInterface() && getContextual(rootClass) != null) {
194+
// We cannot use cache because it may be contextual non-sealed interface serializer,
195+
// but we cannot return result of getContextual() directly either, because rootClass
196+
// can be a sealed interface as well (in that case, rootClass.serializerOrNull() should have priority over getContextual()).
197+
// If we had function like KClass.isNonSealedInterface() we could optimize this place,
198+
// but Native does not provide enough reflection for that. (https://youtrack.jetbrains.com/issue/KT-41339)
199+
null
200+
} else {
201+
findCachedSerializer(rootClass, isNullable)
202+
}
194203
} else {
195-
findParametrizedCachedSerializer(rootClass, typeArguments, isNullable).getOrNull()
204+
// We cannot enable cache even if the current class is non-interface, as it may have interface among type arguments
205+
// and we do not want to waste time scanning them all.
206+
if (hasInterfaceContextualSerializers) {
207+
null
208+
} else {
209+
findParametrizedCachedSerializer(
210+
rootClass,
211+
typeArguments,
212+
isNullable
213+
).getOrNull()
214+
}
196215
}
197-
cachedSerializer?.let { return it }
216+
217+
if (cachedSerializer != null) return cachedSerializer
198218

199219
// slow path to find contextual serializers in serializers module
200220
val contextualSerializer: KSerializer<out Any?>? = if (typeArguments.isEmpty()) {
201-
getContextual(rootClass)
221+
rootClass.serializerOrNull()
222+
?: getContextual(rootClass)
223+
?: rootClass.polymorphicIfInterface()
202224
} else {
203225
val serializers = serializersForParameters(typeArguments, failOnMissingTypeArgSerializer) ?: return null
204226
// first, we look among the built-in serializers, because the parameter could be contextual
205227
rootClass.parametrizedSerializerOrNull(serializers) { typeArguments[0].classifier }
206-
?: getContextual(
207-
rootClass,
208-
serializers
209-
)
228+
?: getContextual(rootClass, serializers)
229+
// PolymorphicSerializer always is returned even for Interface<T>, although it rarely works as expected.
230+
?: rootClass.polymorphicIfInterface()
210231
}
211232
return contextualSerializer?.cast<Any>()?.nullable(isNullable)
212233
}
@@ -376,3 +397,24 @@ internal fun noCompiledSerializer(
376397
): KSerializer<*> {
377398
return module.getContextual(kClass, argSerializers.asList()) ?: kClass.serializerNotRegistered()
378399
}
400+
401+
/**
402+
* Overloads of [moduleThenPolymorphic] should never be called directly.
403+
* Instead, compiler inserts calls to them when intrinsifying [serializer] function.
404+
*
405+
* If no request KClass is an interface, plugin performs call to [moduleThenPolymorphic] to achieve special behavior for interface serializers.
406+
* (They are only serializers that have module priority over default [PolymorphicSerializer]).
407+
*/
408+
@OptIn(ExperimentalSerializationApi::class)
409+
@Suppress("unused")
410+
@PublishedApi
411+
internal fun moduleThenPolymorphic(module: SerializersModule, kClass: KClass<*>): KSerializer<*> {
412+
return module.getContextual(kClass) ?: PolymorphicSerializer(kClass)
413+
}
414+
415+
@OptIn(ExperimentalSerializationApi::class)
416+
@Suppress("unused")
417+
@PublishedApi
418+
internal fun moduleThenPolymorphic(module: SerializersModule, kClass: KClass<*>, argSerializers: Array<KSerializer<*>>): KSerializer<*> {
419+
return module.getContextual(kClass, argSerializers.asList()) ?: PolymorphicSerializer(kClass)
420+
}

core/commonMain/src/kotlinx/serialization/SerializersCache.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package kotlinx.serialization
66

77
import kotlinx.serialization.builtins.nullable
8+
import kotlinx.serialization.internal.*
89
import kotlinx.serialization.internal.cast
910
import kotlinx.serialization.internal.createCache
1011
import kotlinx.serialization.internal.createParametrizedCache
@@ -18,13 +19,13 @@ import kotlin.reflect.KType
1819
* Cache for non-null non-parametrized and non-contextual serializers.
1920
*/
2021
@ThreadLocal
21-
private val SERIALIZERS_CACHE = createCache { it.serializerOrNull() }
22+
private val SERIALIZERS_CACHE = createCache { it.serializerOrNull() ?: it.polymorphicIfInterface() }
2223

2324
/**
2425
* Cache for nullable non-parametrized and non-contextual serializers.
2526
*/
2627
@ThreadLocal
27-
private val SERIALIZERS_CACHE_NULLABLE = createCache<Any?> { it.serializerOrNull()?.nullable?.cast() }
28+
private val SERIALIZERS_CACHE_NULLABLE = createCache<Any?> { (it.serializerOrNull() ?: it.polymorphicIfInterface())?.nullable?.cast() }
2829

2930
/**
3031
* Cache for non-null parametrized and non-contextual serializers.
@@ -72,3 +73,6 @@ internal fun findParametrizedCachedSerializer(
7273
PARAMETRIZED_SERIALIZERS_CACHE_NULLABLE.get(clazz, types)
7374
}
7475
}
76+
77+
@Suppress("NOTHING_TO_INLINE")
78+
internal inline fun KClass<*>.polymorphicIfInterface() = if (this.isInterface()) PolymorphicSerializer(this) else null

core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ internal expect fun BooleanArray.getChecked(index: Int): Boolean
141141

142142
internal expect fun <T : Any> KClass<T>.compiledSerializerImpl(): KSerializer<T>?
143143

144+
internal expect fun <T: Any> KClass<T>.isInterface(): Boolean
145+
144146
/**
145147
* Create serializers cache for non-parametrized and non-contextual serializers.
146148
* The activity and type of cache is determined for a specific platform and a specific environment.

core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ public sealed class SerializersModule {
6767
*/
6868
@ExperimentalSerializationApi
6969
public abstract fun dumpTo(collector: SerializersModuleCollector)
70+
71+
@InternalSerializationApi
72+
internal abstract val hasInterfaceContextualSerializers: Boolean
7073
}
7174

7275
/**
@@ -76,7 +79,14 @@ public sealed class SerializersModule {
7679
level = DeprecationLevel.WARNING,
7780
replaceWith = ReplaceWith("EmptySerializersModule()"))
7881
@JsName("EmptySerializersModuleLegacyJs") // Compatibility with JS
79-
public val EmptySerializersModule: SerializersModule = SerialModuleImpl(emptyMap(), emptyMap(), emptyMap(), emptyMap(), emptyMap())
82+
public val EmptySerializersModule: SerializersModule = SerialModuleImpl(
83+
emptyMap(),
84+
emptyMap(),
85+
emptyMap(),
86+
emptyMap(),
87+
emptyMap(),
88+
false
89+
)
8090

8191
/**
8292
* Returns a combination of two serial modules
@@ -147,7 +157,8 @@ internal class SerialModuleImpl(
147157
@JvmField val polyBase2Serializers: Map<KClass<*>, Map<KClass<*>, KSerializer<*>>>,
148158
private val polyBase2DefaultSerializerProvider: Map<KClass<*>, PolymorphicSerializerProvider<*>>,
149159
private val polyBase2NamedSerializers: Map<KClass<*>, Map<String, KSerializer<*>>>,
150-
private val polyBase2DefaultDeserializerProvider: Map<KClass<*>, PolymorphicDeserializerProvider<*>>
160+
private val polyBase2DefaultDeserializerProvider: Map<KClass<*>, PolymorphicDeserializerProvider<*>>,
161+
internal override val hasInterfaceContextualSerializers: Boolean
151162
) : SerializersModule() {
152163

153164
override fun <T : Any> getPolymorphic(baseClass: KClass<in T>, value: T): SerializationStrategy<T>? {

core/commonMain/src/kotlinx/serialization/modules/SerializersModuleBuilders.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public class SerializersModuleBuilder @PublishedApi internal constructor() : Ser
4949
private val polyBase2DefaultSerializerProvider: MutableMap<KClass<*>, PolymorphicSerializerProvider<*>> = hashMapOf()
5050
private val polyBase2NamedSerializers: MutableMap<KClass<*>, MutableMap<String, KSerializer<*>>> = hashMapOf()
5151
private val polyBase2DefaultDeserializerProvider: MutableMap<KClass<*>, PolymorphicDeserializerProvider<*>> = hashMapOf()
52+
private var hasInterfaceContextualSerializers: Boolean = false
5253

5354
/**
5455
* Adds [serializer] associated with given [kClass] for contextual serialization.
@@ -155,6 +156,7 @@ public class SerializersModuleBuilder @PublishedApi internal constructor() : Ser
155156
}
156157
}
157158
class2ContextualProvider[forClass] = provider
159+
if (forClass.isInterface()) hasInterfaceContextualSerializers = true
158160
}
159161

160162
@JvmName("registerDefaultPolymorphicSerializer") // Don't mangle method name for prettier stack traces
@@ -229,7 +231,7 @@ public class SerializersModuleBuilder @PublishedApi internal constructor() : Ser
229231

230232
@PublishedApi
231233
internal fun build(): SerializersModule =
232-
SerialModuleImpl(class2ContextualProvider, polyBase2Serializers, polyBase2DefaultSerializerProvider, polyBase2NamedSerializers, polyBase2DefaultDeserializerProvider)
234+
SerialModuleImpl(class2ContextualProvider, polyBase2Serializers, polyBase2DefaultSerializerProvider, polyBase2NamedSerializers, polyBase2DefaultDeserializerProvider, hasInterfaceContextualSerializers)
233235
}
234236

235237
/**

0 commit comments

Comments
 (0)