From 456ec8c95ab1cd4e1eb850703113e1d68fd30600 Mon Sep 17 00:00:00 2001 From: Leonid Startsev Date: Thu, 8 Jun 2023 14:15:34 +0200 Subject: [PATCH 1/2] Change priority for PolymorphicSerializer inside serializer() function 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 impact should be minimal, as for most usecases (without modules), serializer will be the same and special workarounds are performed so cache would 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 plugin with changed functionality are expected to be merged in 2.0.0-RC. Fixes #2060 --- core/api/kotlinx-serialization-core.api | 2 + .../src/kotlinx/serialization/Serializers.kt | 60 ++++- .../kotlinx/serialization/SerializersCache.kt | 8 +- .../serialization/internal/Platform.common.kt | 2 + .../modules/SerializersModule.kt | 15 +- .../modules/SerializersModuleBuilders.kt | 4 +- .../InterfaceContextualSerializerTest.kt | 241 ++++++++++++++++++ .../SerializersLookupNamedCompanionTest.kt | 8 +- .../serialization/internal/Platform.kt | 3 +- .../kotlinx/serialization/SerializersJvm.kt | 2 +- .../serialization/internal/Platform.kt | 17 +- .../InterfaceContextualSerializerTestJvm.kt | 78 ++++++ .../serialization/internal/Platform.kt | 1 + .../serialization/internal/Platform.kt | 3 +- gradle.properties | 2 +- integration-test/gradle.properties | 2 +- 16 files changed, 407 insertions(+), 41 deletions(-) create mode 100644 core/commonTest/src/kotlinx/serialization/InterfaceContextualSerializerTest.kt create mode 100644 core/jvmTest/src/kotlinx/serialization/InterfaceContextualSerializerTestJvm.kt diff --git a/core/api/kotlinx-serialization-core.api b/core/api/kotlinx-serialization-core.api index 720e5847e7..c349af2403 100644 --- a/core/api/kotlinx-serialization-core.api +++ b/core/api/kotlinx-serialization-core.api @@ -123,6 +123,8 @@ public abstract interface annotation class kotlinx/serialization/Serializer : ja } public final class kotlinx/serialization/SerializersKt { + public static final fun moduleThenPolymorphic (Lkotlinx/serialization/modules/SerializersModule;Lkotlin/reflect/KClass;)Lkotlinx/serialization/KSerializer; + public static final fun moduleThenPolymorphic (Lkotlinx/serialization/modules/SerializersModule;Lkotlin/reflect/KClass;[Lkotlinx/serialization/KSerializer;)Lkotlinx/serialization/KSerializer; public static final fun noCompiledSerializer (Ljava/lang/String;)Lkotlinx/serialization/KSerializer; public static final fun noCompiledSerializer (Lkotlinx/serialization/modules/SerializersModule;Lkotlin/reflect/KClass;)Lkotlinx/serialization/KSerializer; public static final fun noCompiledSerializer (Lkotlinx/serialization/modules/SerializersModule;Lkotlin/reflect/KClass;[Lkotlinx/serialization/KSerializer;)Lkotlinx/serialization/KSerializer; diff --git a/core/commonMain/src/kotlinx/serialization/Serializers.kt b/core/commonMain/src/kotlinx/serialization/Serializers.kt index 2489be276f..4e44d3d2b2 100644 --- a/core/commonMain/src/kotlinx/serialization/Serializers.kt +++ b/core/commonMain/src/kotlinx/serialization/Serializers.kt @@ -189,24 +189,45 @@ private fun SerializersModule.serializerByKTypeImpl( val isNullable = type.isMarkedNullable val typeArguments = type.arguments.map(KTypeProjection::typeOrThrow) - val cachedSerializer = if (typeArguments.isEmpty()) { - findCachedSerializer(rootClass, isNullable) + val cachedSerializer = if (typeArguments.isEmpty()) { + if (rootClass.isInterface() && getContextual(rootClass) != null) { + // We cannot use cache because it may be contextual non-sealed interface serializer, + // but we cannot return result of getContextual() directly either, because rootClass + // can be a sealed interface as well (in that case, rootClass.serializerOrNull() should have priority over getContextual()). + // If we had function like KClass.isNonSealedInterface() we could optimize this place, + // but Native does not provide enough reflection for that. (https://youtrack.jetbrains.com/issue/KT-41339) + null + } else { + findCachedSerializer(rootClass, isNullable) + } } else { - findParametrizedCachedSerializer(rootClass, typeArguments, isNullable).getOrNull() + // We cannot enable cache even if the current class is non-interface, as it may have interface among type arguments + // and we do not want to waste time scanning them all. + if (hasInterfaceContextualSerializers) { + null + } else { + findParametrizedCachedSerializer( + rootClass, + typeArguments, + isNullable + ).getOrNull() + } } - cachedSerializer?.let { return it } + + if (cachedSerializer != null) return cachedSerializer // slow path to find contextual serializers in serializers module val contextualSerializer: KSerializer? = if (typeArguments.isEmpty()) { - getContextual(rootClass) + rootClass.serializerOrNull() + ?: getContextual(rootClass) + ?: rootClass.polymorphicIfInterface() } else { val serializers = serializersForParameters(typeArguments, failOnMissingTypeArgSerializer) ?: return null // first, we look among the built-in serializers, because the parameter could be contextual rootClass.parametrizedSerializerOrNull(serializers) { typeArguments[0].classifier } - ?: getContextual( - rootClass, - serializers - ) + ?: getContextual(rootClass, serializers) + // PolymorphicSerializer always is returned even for Interface, although it rarely works as expected. + ?: rootClass.polymorphicIfInterface() } return contextualSerializer?.cast()?.nullable(isNullable) } @@ -376,3 +397,24 @@ internal fun noCompiledSerializer( ): KSerializer<*> { return module.getContextual(kClass, argSerializers.asList()) ?: kClass.serializerNotRegistered() } + +/** + * Overloads of [moduleThenPolymorphic] should never be called directly. + * Instead, compiler inserts calls to them when intrinsifying [serializer] function. + * + * If no request KClass is an interface, plugin performs call to [moduleThenPolymorphic] to achieve special behavior for interface serializers. + * (They are only serializers that have module priority over default [PolymorphicSerializer]). + */ +@OptIn(ExperimentalSerializationApi::class) +@Suppress("unused") +@PublishedApi +internal fun moduleThenPolymorphic(module: SerializersModule, kClass: KClass<*>): KSerializer<*> { + return module.getContextual(kClass) ?: PolymorphicSerializer(kClass) +} + +@OptIn(ExperimentalSerializationApi::class) +@Suppress("unused") +@PublishedApi +internal fun moduleThenPolymorphic(module: SerializersModule, kClass: KClass<*>, argSerializers: Array>): KSerializer<*> { + return module.getContextual(kClass, argSerializers.asList()) ?: PolymorphicSerializer(kClass) +} diff --git a/core/commonMain/src/kotlinx/serialization/SerializersCache.kt b/core/commonMain/src/kotlinx/serialization/SerializersCache.kt index cc86e4358d..d211d4d376 100644 --- a/core/commonMain/src/kotlinx/serialization/SerializersCache.kt +++ b/core/commonMain/src/kotlinx/serialization/SerializersCache.kt @@ -5,6 +5,7 @@ package kotlinx.serialization import kotlinx.serialization.builtins.nullable +import kotlinx.serialization.internal.* import kotlinx.serialization.internal.cast import kotlinx.serialization.internal.createCache import kotlinx.serialization.internal.createParametrizedCache @@ -18,13 +19,13 @@ import kotlin.reflect.KType * Cache for non-null non-parametrized and non-contextual serializers. */ @ThreadLocal -private val SERIALIZERS_CACHE = createCache { it.serializerOrNull() } +private val SERIALIZERS_CACHE = createCache { it.serializerOrNull() ?: it.polymorphicIfInterface() } /** * Cache for nullable non-parametrized and non-contextual serializers. */ @ThreadLocal -private val SERIALIZERS_CACHE_NULLABLE = createCache { it.serializerOrNull()?.nullable?.cast() } +private val SERIALIZERS_CACHE_NULLABLE = createCache { (it.serializerOrNull() ?: it.polymorphicIfInterface())?.nullable?.cast() } /** * Cache for non-null parametrized and non-contextual serializers. @@ -72,3 +73,6 @@ internal fun findParametrizedCachedSerializer( PARAMETRIZED_SERIALIZERS_CACHE_NULLABLE.get(clazz, types) } } + +@Suppress("NOTHING_TO_INLINE") +internal inline fun KClass<*>.polymorphicIfInterface() = if (this.isInterface()) PolymorphicSerializer(this) else null diff --git a/core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt b/core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt index ef313ccd97..b6f1e96e99 100644 --- a/core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt +++ b/core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt @@ -141,6 +141,8 @@ internal expect fun BooleanArray.getChecked(index: Int): Boolean internal expect fun KClass.compiledSerializerImpl(): KSerializer? +internal expect fun KClass.isInterface(): Boolean + /** * Create serializers cache for non-parametrized and non-contextual serializers. * The activity and type of cache is determined for a specific platform and a specific environment. diff --git a/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt b/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt index 8a9126d747..612f542c2e 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt @@ -67,6 +67,9 @@ public sealed class SerializersModule { */ @ExperimentalSerializationApi public abstract fun dumpTo(collector: SerializersModuleCollector) + + @InternalSerializationApi + internal abstract val hasInterfaceContextualSerializers: Boolean } /** @@ -76,7 +79,14 @@ public sealed class SerializersModule { level = DeprecationLevel.WARNING, replaceWith = ReplaceWith("EmptySerializersModule()")) @JsName("EmptySerializersModuleLegacyJs") // Compatibility with JS -public val EmptySerializersModule: SerializersModule = SerialModuleImpl(emptyMap(), emptyMap(), emptyMap(), emptyMap(), emptyMap()) +public val EmptySerializersModule: SerializersModule = SerialModuleImpl( + emptyMap(), + emptyMap(), + emptyMap(), + emptyMap(), + emptyMap(), + false +) /** * Returns a combination of two serial modules @@ -147,7 +157,8 @@ internal class SerialModuleImpl( @JvmField val polyBase2Serializers: Map, Map, KSerializer<*>>>, private val polyBase2DefaultSerializerProvider: Map, PolymorphicSerializerProvider<*>>, private val polyBase2NamedSerializers: Map, Map>>, - private val polyBase2DefaultDeserializerProvider: Map, PolymorphicDeserializerProvider<*>> + private val polyBase2DefaultDeserializerProvider: Map, PolymorphicDeserializerProvider<*>>, + internal override val hasInterfaceContextualSerializers: Boolean ) : SerializersModule() { override fun getPolymorphic(baseClass: KClass, value: T): SerializationStrategy? { diff --git a/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleBuilders.kt b/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleBuilders.kt index dfb9d819e3..451e3268a7 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleBuilders.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleBuilders.kt @@ -49,6 +49,7 @@ public class SerializersModuleBuilder @PublishedApi internal constructor() : Ser private val polyBase2DefaultSerializerProvider: MutableMap, PolymorphicSerializerProvider<*>> = hashMapOf() private val polyBase2NamedSerializers: MutableMap, MutableMap>> = hashMapOf() private val polyBase2DefaultDeserializerProvider: MutableMap, PolymorphicDeserializerProvider<*>> = hashMapOf() + private var hasInterfaceContextualSerializers: Boolean = false /** * Adds [serializer] associated with given [kClass] for contextual serialization. @@ -155,6 +156,7 @@ public class SerializersModuleBuilder @PublishedApi internal constructor() : Ser } } class2ContextualProvider[forClass] = provider + if (forClass.isInterface()) hasInterfaceContextualSerializers = true } @JvmName("registerDefaultPolymorphicSerializer") // Don't mangle method name for prettier stack traces @@ -229,7 +231,7 @@ public class SerializersModuleBuilder @PublishedApi internal constructor() : Ser @PublishedApi internal fun build(): SerializersModule = - SerialModuleImpl(class2ContextualProvider, polyBase2Serializers, polyBase2DefaultSerializerProvider, polyBase2NamedSerializers, polyBase2DefaultDeserializerProvider) + SerialModuleImpl(class2ContextualProvider, polyBase2Serializers, polyBase2DefaultSerializerProvider, polyBase2NamedSerializers, polyBase2DefaultDeserializerProvider, hasInterfaceContextualSerializers) } /** diff --git a/core/commonTest/src/kotlinx/serialization/InterfaceContextualSerializerTest.kt b/core/commonTest/src/kotlinx/serialization/InterfaceContextualSerializerTest.kt new file mode 100644 index 0000000000..fbee4bfe73 --- /dev/null +++ b/core/commonTest/src/kotlinx/serialization/InterfaceContextualSerializerTest.kt @@ -0,0 +1,241 @@ +/* + * Copyright 2017-2023 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.encoding.* +import kotlinx.serialization.modules.* +import kotlinx.serialization.test.* +import kotlin.reflect.* +import kotlin.test.* + +// Imagine this is a 3rd party interface +interface IApiError { + val code: Int +} + +@Serializable(CustomSer::class) +interface HasCustom + + +object CustomSer: KSerializer { + override val descriptor: SerialDescriptor + get() = TODO("Not yet implemented") + + override fun serialize(encoder: Encoder, value: HasCustom) { + TODO("Not yet implemented") + } + + override fun deserialize(decoder: Decoder): HasCustom { + TODO("Not yet implemented") + } +} + +@Suppress("UNCHECKED_CAST") +class InterfaceContextualSerializerTest { + + @Serializable + data class Box(val boxed: T) + + object MyApiErrorSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("IApiError", PrimitiveKind.INT) + + override fun serialize(encoder: Encoder, value: IApiError) { + encoder.encodeInt(value.code) + } + + override fun deserialize(decoder: Decoder): IApiError { + val code = decoder.decodeInt() + return object : IApiError { + override val code: Int = code + } + } + } + + private inline fun SerializersModule.doTest(block: (KSerializer) -> Unit) { + block(this.serializer()) + block(this.serializer(typeOf()) as KSerializer) + } + + // Native, WASM - can't retrieve serializer (no .isInterface) + @Test + fun testDefault() { + if (isNative() || isWasm()) return + assertEquals(PolymorphicKind.OPEN, serializer().descriptor.kind) + assertEquals(PolymorphicKind.OPEN, serializer(typeOf()).descriptor.kind) + } + + @Test + fun testCustom() { + assertSame(CustomSer, serializer()) + assertSame(CustomSer, serializer(typeOf()) as KSerializer) + } + + // JVM - intrinsics kick in + @Test + fun testContextual() { + val module = serializersModuleOf(IApiError::class, MyApiErrorSerializer) + assertSame(MyApiErrorSerializer, module.serializer(typeOf()) as KSerializer) + shouldFail(beforeKotlin = "2.0.0", onJvm = true, onWasm = false, onNative = false, onJs = false ) { + assertSame(MyApiErrorSerializer, module.serializer()) + } + } + + // JVM - intrinsics kick in + @Test + fun testInsideList() { + val module = serializersModuleOf(IApiError::class, MyApiErrorSerializer) + assertEquals(MyApiErrorSerializer.descriptor, module.serializer(typeOf>()).descriptor.elementDescriptors.first()) + shouldFail(beforeKotlin = "2.0.0", onWasm = false, onNative = false, onJs = false ) { + assertEquals( + MyApiErrorSerializer.descriptor, + module.serializer>().descriptor.elementDescriptors.first() + ) + } + } + + @Test + fun testInsideBox() { + val module = serializersModuleOf(IApiError::class, MyApiErrorSerializer) + assertEquals(MyApiErrorSerializer.descriptor, module.serializer(typeOf>()).descriptor.elementDescriptors.first()) + shouldFail(beforeKotlin = "2.0.0", onWasm = false, onNative = false, onJs = false ) { + assertEquals( + MyApiErrorSerializer.descriptor, + module.serializer>().descriptor.elementDescriptors.first() + ) + } + } + + @Test + fun testWithNullability() { + val module = serializersModuleOf(IApiError::class, MyApiErrorSerializer) + assertEquals(MyApiErrorSerializer.nullable.descriptor, module.serializer(typeOf()).descriptor) + shouldFail(beforeKotlin = "2.0.0", onWasm = false, onNative = false, onJs = false ) { + assertEquals(MyApiErrorSerializer.nullable.descriptor, module.serializer().descriptor) + } + } + + @Test + fun testWithNullabilityInsideList() { + val module = serializersModuleOf(IApiError::class, MyApiErrorSerializer) + assertEquals(MyApiErrorSerializer.nullable.descriptor, module.serializer(typeOf>()).descriptor.elementDescriptors.first()) + shouldFail(beforeKotlin = "2.0.0", onWasm = false, onNative = false, onJs = false ) { + assertEquals( + MyApiErrorSerializer.nullable.descriptor, + module.serializer>().descriptor.elementDescriptors.first() + ) + } + } + + class Unrelated + + object UnrelatedSerializer: KSerializer { + override val descriptor: SerialDescriptor + get() = TODO("Not yet implemented") + + override fun serialize(encoder: Encoder, value: Unrelated) { + TODO("Not yet implemented") + } + + override fun deserialize(decoder: Decoder): Unrelated { + TODO("Not yet implemented") + } + } + + @Test + fun interfaceSerializersAreCachedInsideIfModuleIsNotFilledWithInterface() = jvmOnly { + // Caches are implemented on JVM + val module = serializersModuleOf(Unrelated::class, UnrelatedSerializer) + val p1 = module.serializer(typeOf>()) + assertEquals(PolymorphicKind.OPEN, p1.descriptor.elementDescriptors.first().kind) + val p2 = module.serializer(typeOf>()) + assertSame(p1, p2) + } + + @Test + fun interfaceSerializersAreCachedTopLevelIfModuleIsNotFilledWithInterface() = jvmOnly { + val module = serializersModuleOf(Unrelated::class, UnrelatedSerializer) + val p1 = module.serializer(typeOf()) + assertEquals(PolymorphicKind.OPEN, p1.descriptor.kind) + val p2 = module.serializer(typeOf()) + assertSame(p1, p2) + } + + interface Parametrized { + val param: List + } + + class PSer(val tSer: KSerializer): KSerializer> { + override val descriptor: SerialDescriptor + get() = buildClassSerialDescriptor("PSer<${tSer.descriptor.serialName}>") + + override fun serialize(encoder: Encoder, value: Parametrized) { + TODO("Not yet implemented") + } + + override fun deserialize(decoder: Decoder): Parametrized { + TODO("Not yet implemented") + } + } + + @Test + fun testParametrizedInterface() { + if (!isNative() && !isWasm()) { + assertEquals(PolymorphicKind.OPEN, serializer(typeOf>()).descriptor.kind) + } + val md = SerializersModule { + contextual(Parametrized::class) { PSer(it[0]) } + } + assertEquals("PSer", md.serializer(typeOf>()).descriptor.serialName) + shouldFail(beforeKotlin = "2.0.0", onWasm = false, onNative = false, onJs = false ) { + assertEquals("PSer", md.serializer>().descriptor.serialName) + } + } + + @Serializable + sealed interface SealedI + + @Test + fun sealedInterfacesAreNotAffected() { + val module = serializersModuleOf(IApiError::class, MyApiErrorSerializer) + module.doTest { + assertEquals(PolymorphicKind.SEALED, it.descriptor.kind) + } + module.doTest> { + assertEquals(PolymorphicKind.SEALED, it.descriptor.elementDescriptors.first().kind) + } + module.doTest> { + assertEquals(PolymorphicKind.SEALED, it.descriptor.elementDescriptors.first().kind) + } + } + + object SealedSer: KSerializer { + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor("SealedSer", PrimitiveKind.INT) + + override fun serialize(encoder: Encoder, value: SealedI) { + TODO("Not yet implemented") + } + + override fun deserialize(decoder: Decoder): SealedI { + TODO("Not yet implemented") + } + } + + @Test + fun sealedInterfacesAreNotOverriden() { + val module = serializersModuleOf(SealedI::class, SealedSer) + module.doTest { + assertEquals(PolymorphicKind.SEALED, it.descriptor.kind) + } + module.doTest> { + assertEquals(PolymorphicKind.SEALED, it.descriptor.elementDescriptors.first().kind) + } + module.doTest> { + assertEquals(PolymorphicKind.SEALED, it.descriptor.elementDescriptors.first().kind) + } + } +} diff --git a/core/commonTest/src/kotlinx/serialization/SerializersLookupNamedCompanionTest.kt b/core/commonTest/src/kotlinx/serialization/SerializersLookupNamedCompanionTest.kt index 65324c4c76..8611d34d2e 100644 --- a/core/commonTest/src/kotlinx/serialization/SerializersLookupNamedCompanionTest.kt +++ b/core/commonTest/src/kotlinx/serialization/SerializersLookupNamedCompanionTest.kt @@ -88,13 +88,7 @@ class SerializersLookupNamedCompanionTest { serializer(typeOf()).descriptor.toString() ) } - - // should fail because annotation @NamedCompanion will be placed again by the compilation plugin - // and they both will be placed into @Container annotation - thus they will be invisible to the runtime - shouldFail(sinceKotlin = "1.9.20", onJs = false, onNative = false, onWasm = false) { - serializer(typeOf()) - } } -} \ No newline at end of file +} diff --git a/core/jsMain/src/kotlinx/serialization/internal/Platform.kt b/core/jsMain/src/kotlinx/serialization/internal/Platform.kt index 6bd63391b4..a191dc1bf7 100644 --- a/core/jsMain/src/kotlinx/serialization/internal/Platform.kt +++ b/core/jsMain/src/kotlinx/serialization/internal/Platform.kt @@ -23,6 +23,8 @@ internal actual fun KClass.compiledSerializerImpl(): KSerializer else this.js.asDynamic().Companion?.serializer() ) as? KSerializer +internal actual fun KClass.isInterface(): Boolean = isInterface + internal actual fun createCache(factory: (KClass<*>) -> KSerializer?): SerializerCache { return object: SerializerCache { override fun get(key: KClass): KSerializer? { @@ -56,7 +58,6 @@ internal actual fun KClass.constructSerializerForGivenTypeArgs(vara when { assocObject is KSerializer<*> -> assocObject as KSerializer assocObject is SerializerFactory -> assocObject.serializer(*args) as KSerializer - this.isInterface -> PolymorphicSerializer(this) else -> null } } catch (e: dynamic) { diff --git a/core/jvmMain/src/kotlinx/serialization/SerializersJvm.kt b/core/jvmMain/src/kotlinx/serialization/SerializersJvm.kt index b2d8da7c77..2e9e2d04d2 100644 --- a/core/jvmMain/src/kotlinx/serialization/SerializersJvm.kt +++ b/core/jvmMain/src/kotlinx/serialization/SerializersJvm.kt @@ -168,7 +168,7 @@ private fun SerializersModule.reflectiveOrContextual( ): KSerializer? { jClass.constructSerializerForGivenTypeArgs(*typeArgumentsSerializers.toTypedArray())?.let { return it } val kClass = jClass.kotlin - return kClass.builtinSerializerOrNull() ?: getContextual(kClass, typeArgumentsSerializers) + return kClass.builtinSerializerOrNull() ?: getContextual(kClass, typeArgumentsSerializers) ?: if (jClass.isInterface) PolymorphicSerializer(jClass.kotlin) else null } @OptIn(ExperimentalSerializationApi::class) diff --git a/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt b/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt index 72ec9ea968..5cf32d0e5d 100644 --- a/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt +++ b/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt @@ -18,6 +18,8 @@ internal actual inline fun BooleanArray.getChecked(index: Int): Boolean { return get(index) } +internal actual fun KClass.isInterface(): Boolean = java.isInterface + internal actual fun KClass.compiledSerializerImpl(): KSerializer? = this.constructSerializerForGivenTypeArgs() @@ -39,8 +41,6 @@ internal fun Class.constructSerializerForGivenTypeArgs(vararg args: if (isEnum && isNotAnnotated()) { return createEnumSerializer() } - // Fall-through if the serializer is not found -- lookup on companions (for sealed interfaces) or fallback to polymorphic if applicable - if (isInterface) interfaceSerializer()?.let { return it } // Search for serializer defined on companion object. val serializer = invokeSerializerOnDefaultCompanion(this, *args) if (serializer != null) return serializer @@ -105,19 +105,6 @@ private fun Class.isPolymorphicSerializer(): Boolean { return false } -private fun Class.interfaceSerializer(): KSerializer? { - /* - * Interfaces are @Polymorphic by default. - * Check if it has no annotations or `@Serializable(with = PolymorphicSerializer::class)`, - * otherwise bailout. - */ - val serializable = getAnnotation(Serializable::class.java) - if (serializable == null || serializable.with == PolymorphicSerializer::class) { - return PolymorphicSerializer(this.kotlin) - } - return null -} - private fun invokeSerializerOnDefaultCompanion(jClass: Class<*>, vararg args: KSerializer): KSerializer? { val companion = jClass.companionOrNull("Companion") ?: return null return invokeSerializerOnCompanion(companion, *args) diff --git a/core/jvmTest/src/kotlinx/serialization/InterfaceContextualSerializerTestJvm.kt b/core/jvmTest/src/kotlinx/serialization/InterfaceContextualSerializerTestJvm.kt new file mode 100644 index 0000000000..47e4150b0e --- /dev/null +++ b/core/jvmTest/src/kotlinx/serialization/InterfaceContextualSerializerTestJvm.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization + +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* +import kotlinx.serialization.modules.* +import kotlinx.serialization.test.* +import kotlinx.serialization.test.shouldFail +import org.junit.Test +import kotlin.reflect.* +import kotlin.test.* + +interface JApiError { + val code: Int +} + +class InterfaceContextualSerializerTestJvm { + object MyApiErrorSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("JApiError", PrimitiveKind.INT) + + override fun serialize(encoder: Encoder, value: JApiError) { + encoder.encodeInt(value.code) + } + + override fun deserialize(decoder: Decoder): JApiError { + val code = decoder.decodeInt() + return object : JApiError { + override val code: Int = code + } + } + } + + @Test + fun testDefault() { + assertEquals(PolymorphicKind.OPEN, serializer(typeTokenOf()).descriptor.kind) + } + + @Test + fun testContextual() { + val module = serializersModuleOf(JApiError::class, MyApiErrorSerializer) + assertSame(MyApiErrorSerializer, module.serializer(typeTokenOf()) as KSerializer) + } + + @Test + fun testInsideList() { + val module = serializersModuleOf(JApiError::class, MyApiErrorSerializer) + assertSame(MyApiErrorSerializer.descriptor, module.serializer(typeTokenOf>()).descriptor.elementDescriptors.first()) + } + + interface Parametrized { + val param: List + } + + class PSer(val tSer: KSerializer): KSerializer> { + override val descriptor: SerialDescriptor + get() = buildClassSerialDescriptor("PSer<${tSer.descriptor.serialName}>") + + override fun serialize(encoder: Encoder, value: Parametrized) { + TODO("Not yet implemented") + } + + override fun deserialize(decoder: Decoder): Parametrized { + TODO("Not yet implemented") + } + } + + @Test + fun testParametrizedInterface() { + assertEquals(PolymorphicKind.OPEN, serializer(typeTokenOf>()).descriptor.kind) + val md = SerializersModule { + contextual(Parametrized::class) { PSer(it[0]) } + } + assertEquals("PSer", md.serializer(typeTokenOf>()).descriptor.serialName) + } +} diff --git a/core/nativeMain/src/kotlinx/serialization/internal/Platform.kt b/core/nativeMain/src/kotlinx/serialization/internal/Platform.kt index 2c91769a66..ddf690c25c 100644 --- a/core/nativeMain/src/kotlinx/serialization/internal/Platform.kt +++ b/core/nativeMain/src/kotlinx/serialization/internal/Platform.kt @@ -41,6 +41,7 @@ internal actual fun KClass.constructSerializerForGivenTypeArgs(vara internal actual fun KClass.compiledSerializerImpl(): KSerializer? = this.constructSerializerForGivenTypeArgs() +internal actual fun KClass.isInterface(): Boolean = false // we do not know, but also PolymorphicSerializer is never returned on Native for interfaces internal actual fun createCache(factory: (KClass<*>) -> KSerializer?): SerializerCache { return object: SerializerCache { diff --git a/core/wasmMain/src/kotlinx/serialization/internal/Platform.kt b/core/wasmMain/src/kotlinx/serialization/internal/Platform.kt index 310df028ab..146845be3a 100644 --- a/core/wasmMain/src/kotlinx/serialization/internal/Platform.kt +++ b/core/wasmMain/src/kotlinx/serialization/internal/Platform.kt @@ -40,6 +40,7 @@ internal actual fun KClass.constructSerializerForGivenTypeArgs(vara internal actual fun KClass.compiledSerializerImpl(): KSerializer? = this.constructSerializerForGivenTypeArgs() +internal actual fun KClass.isInterface(): Boolean = false // we do not know, but also PolymorphicSerializer is never returned on WASM for interfaces internal actual fun createCache(factory: (KClass<*>) -> KSerializer?): SerializerCache { return object: SerializerCache { @@ -59,4 +60,4 @@ internal actual fun createParametrizedCache(factory: (KClass, List ArrayList.toNativeArrayImpl(eClass: KClass): Array = toTypedArray() -internal actual fun isReferenceArray(rootClass: KClass): Boolean = rootClass == Array::class \ No newline at end of file +internal actual fun isReferenceArray(rootClass: KClass): Boolean = rootClass == Array::class diff --git a/gradle.properties b/gradle.properties index 099a38f63c..857f3f223f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ # group=org.jetbrains.kotlinx -version=1.6.4-SNAPSHOT +version=1.7.0-SNAPSHOT kotlin.version=1.9.22 diff --git a/integration-test/gradle.properties b/integration-test/gradle.properties index d29c5df2e5..933feba571 100644 --- a/integration-test/gradle.properties +++ b/integration-test/gradle.properties @@ -3,7 +3,7 @@ # mainKotlinVersion=1.9.22 -mainLibVersion=1.6.4-SNAPSHOT +mainLibVersion=1.7.0-SNAPSHOT kotlin.code.style=official kotlin.js.compiler=ir From 44fedf961bc1b78f651a825742e4f71297ec2447 Mon Sep 17 00:00:00 2001 From: Leonid Startsev Date: Mon, 18 Mar 2024 15:19:38 +0100 Subject: [PATCH 2/2] ~Fix test for Nothing on JS --- .../src/kotlinx/serialization/internal/Platform.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/core/jsMain/src/kotlinx/serialization/internal/Platform.kt b/core/jsMain/src/kotlinx/serialization/internal/Platform.kt index a191dc1bf7..571c3f7979 100644 --- a/core/jsMain/src/kotlinx/serialization/internal/Platform.kt +++ b/core/jsMain/src/kotlinx/serialization/internal/Platform.kt @@ -19,7 +19,7 @@ internal actual fun BooleanArray.getChecked(index: Int): Boolean { internal actual fun KClass.compiledSerializerImpl(): KSerializer? = this.constructSerializerForGivenTypeArgs() ?: ( - if (this === Nothing::class) NothingSerializer // Workaround for KT-51333 + if (this === Nothing::class) NothingSerializer // .js throws an exception for Nothing else this.js.asDynamic().Companion?.serializer() ) as? KSerializer @@ -71,5 +71,9 @@ internal actual fun isReferenceArray(rootClass: KClass): Boolean = rootClas * * Should be eventually replaced with compiler intrinsics */ -private val KClass<*>.isInterface - get(): Boolean = js.asDynamic().`$metadata$`?.kind == "interface" +private val KClass<*>.isInterface: Boolean + get(): Boolean { + // .js throws an exception for Nothing + if (this === Nothing::class) return false + return js.asDynamic().`$metadata$`?.kind == "interface" + }