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..571c3f7979 100644 --- a/core/jsMain/src/kotlinx/serialization/internal/Platform.kt +++ b/core/jsMain/src/kotlinx/serialization/internal/Platform.kt @@ -19,10 +19,12 @@ 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 +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) { @@ -70,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" + } 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