From e6d4a149f1552bcbe7c67084a9c8c3f4975c33d7 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 2 Mar 2020 18:40:36 +0300 Subject: [PATCH] Make SerialModule sealed class instead of interface * As explained in #697, it appears that concatenating two modules as "behaviours" (as opposed to "bag of serializers") is way too dangerous for the end users and implies a lot of inconsistent behaviours and performance hits. So the most rational solution here is to prohibit such concatenations by closing the way to implement your own SerialModule * In order to still solve "fallback serializer" use-case, new 'default' DSL is introduced along with its support in PolymorphicSerializer Fixes #697 Fixes #448 --- gradle/native_mpp.gradle | 106 +++++++++--------- .../kotlinx/serialization/SealedSerializer.kt | 2 +- .../internal/AbstractPolymorphicSerializer.kt | 3 +- .../kotlinx/serialization/internal/Util.kt | 4 + .../json/JsonTransformingSerializer.kt | 9 +- .../json/internal/ContextValidator.kt | 7 ++ .../modules/PolymorphicModuleBuilder.kt | 21 ++++ .../serialization/modules/SerialModule.kt | 106 +++++++++++++++--- .../modules/SerialModuleBuilders.kt | 28 ++++- .../modules/SerialModuleCollector.kt | 8 ++ .../modules/SerialModuleExtensions.kt | 7 ++ .../serialization/modules/SerialModuleImpl.kt | 62 ---------- .../serialization/PolymorphismTestData.kt | 8 +- .../features/PolymorphismTest.kt | 44 +++++++- .../modules/ModuleBuildersTest.kt | 30 +++++ 15 files changed, 299 insertions(+), 146 deletions(-) delete mode 100644 runtime/commonMain/src/kotlinx/serialization/modules/SerialModuleImpl.kt diff --git a/gradle/native_mpp.gradle b/gradle/native_mpp.gradle index 877a7f045b..48af3854d1 100644 --- a/gradle/native_mpp.gradle +++ b/gradle/native_mpp.gradle @@ -10,57 +10,57 @@ project.ext.nativeMainSets = [] project.ext.nativeTestSets = [] kotlin { - targets.metaClass.addTarget = { preset -> - def target = delegate.fromPreset(preset, preset.name) - project.ext.nativeMainSets.add(target.compilations['main'].kotlinSourceSets.first()) - project.ext.nativeTestSets.add(target.compilations['test'].kotlinSourceSets.first()) - } - - targets { - if (project.ext.singleTargetMode) { - fromPreset(project.ext.ideaPreset, 'native') - } else { - // Linux - addTarget(presets.linuxX64) - addTarget(presets.linuxArm32Hfp) - addTarget(presets.linuxArm64) - - // Mac & iOS - addTarget(presets.macosX64) - - addTarget(presets.iosArm64) - addTarget(presets.iosArm32) - addTarget(presets.iosX64) - - addTarget(presets.watchosX86) - addTarget(presets.watchosArm32) - addTarget(presets.watchosArm64) - - addTarget(presets.tvosArm64) - addTarget(presets.tvosX64) - - // Windows - addTarget(presets.mingwX64) - addTarget(presets.mingwX86) - - // WASM - addTarget(presets.wasm32) - } - } - - sourceSets { - nativeMain { dependsOn commonMain } - // Empty source set is required in order to have native tests task - nativeTest {} - - if (!project.ext.singleTargetMode) { - configure(project.ext.nativeMainSets) { - dependsOn nativeMain - } - - configure(project.ext.nativeTestSets) { - dependsOn nativeTest - } - } - } +// targets.metaClass.addTarget = { preset -> +// def target = delegate.fromPreset(preset, preset.name) +// project.ext.nativeMainSets.add(target.compilations['main'].kotlinSourceSets.first()) +// project.ext.nativeTestSets.add(target.compilations['test'].kotlinSourceSets.first()) +// } +// +// targets { +// if (project.ext.singleTargetMode) { +// fromPreset(project.ext.ideaPreset, 'native') +// } else { +// // Linux +// addTarget(presets.linuxX64) +// addTarget(presets.linuxArm32Hfp) +// addTarget(presets.linuxArm64) +// +// // Mac & iOS +// addTarget(presets.macosX64) +// +// addTarget(presets.iosArm64) +// addTarget(presets.iosArm32) +// addTarget(presets.iosX64) +// +// addTarget(presets.watchosX86) +// addTarget(presets.watchosArm32) +// addTarget(presets.watchosArm64) +// +// addTarget(presets.tvosArm64) +// addTarget(presets.tvosX64) +// +// // Windows +// addTarget(presets.mingwX64) +// addTarget(presets.mingwX86) +// +// // WASM +// addTarget(presets.wasm32) +// } +// } +// +// sourceSets { +// nativeMain { dependsOn commonMain } +// // Empty source set is required in order to have native tests task +// nativeTest {} +// +// if (!project.ext.singleTargetMode) { +// configure(project.ext.nativeMainSets) { +// dependsOn nativeMain +// } +// +// configure(project.ext.nativeTestSets) { +// dependsOn nativeTest +// } +// } +// } } diff --git a/runtime/commonMain/src/kotlinx/serialization/SealedSerializer.kt b/runtime/commonMain/src/kotlinx/serialization/SealedSerializer.kt index c4a2bfc677..da6e980c42 100644 --- a/runtime/commonMain/src/kotlinx/serialization/SealedSerializer.kt +++ b/runtime/commonMain/src/kotlinx/serialization/SealedSerializer.kt @@ -100,7 +100,7 @@ public class SealedClassSerializer( }.mapValues { it.value.value } } - override fun findPolymorphicSerializer(decoder: CompositeDecoder, klassName: String): KSerializer { + override fun findPolymorphicSerializer(decoder: CompositeDecoder, klassName: String): DeserializationStrategy { return serialName2Serializer[klassName] ?: super.findPolymorphicSerializer(decoder, klassName) } diff --git a/runtime/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt b/runtime/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt index 1d162c4515..98a4bb5797 100644 --- a/runtime/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt +++ b/runtime/commonMain/src/kotlinx/serialization/internal/AbstractPolymorphicSerializer.kt @@ -81,7 +81,8 @@ public abstract class AbstractPolymorphicSerializer internal constructo public open fun findPolymorphicSerializer( decoder: CompositeDecoder, klassName: String - ): KSerializer = decoder.context.getPolymorphic(baseClass, klassName) + ): DeserializationStrategy = decoder.context.getPolymorphic(baseClass, klassName) + ?: decoder.context.getDefaultPolymorphic(baseClass, klassName) ?: throwSubtypeNotRegistered(klassName, baseClass) diff --git a/runtime/commonMain/src/kotlinx/serialization/internal/Util.kt b/runtime/commonMain/src/kotlinx/serialization/internal/Util.kt index 4fd73bac75..cd52a5918f 100644 --- a/runtime/commonMain/src/kotlinx/serialization/internal/Util.kt +++ b/runtime/commonMain/src/kotlinx/serialization/internal/Util.kt @@ -113,3 +113,7 @@ internal fun defer(deferred: () -> SerialDescriptor): SerialDescriptor = object @Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE") @PublishedApi internal inline fun KSerializer<*>.cast(): KSerializer = this as KSerializer + +@Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE") +@PublishedApi +internal inline fun DeserializationStrategy<*>.cast(): DeserializationStrategy = this as DeserializationStrategy diff --git a/runtime/commonMain/src/kotlinx/serialization/json/JsonTransformingSerializer.kt b/runtime/commonMain/src/kotlinx/serialization/json/JsonTransformingSerializer.kt index c70184facb..1f23431543 100644 --- a/runtime/commonMain/src/kotlinx/serialization/json/JsonTransformingSerializer.kt +++ b/runtime/commonMain/src/kotlinx/serialization/json/JsonTransformingSerializer.kt @@ -80,19 +80,18 @@ public abstract class JsonTransformingSerializer( final override fun deserialize(decoder: Decoder): T { val input = decoder.asJsonInput() - var element = input.decodeJson() - element = readTransform(element) - return input.json.fromJson(tSerializer, element) + val element = input.decodeJson() + return input.json.fromJson(tSerializer, readTransform(element)) } /** - * Transformation which happens during [serialize] call. + * Transformation that happens during [serialize] call. * Does nothing by default. */ protected open fun readTransform(element: JsonElement): JsonElement = element /** - * Transformation which happens during [deserialize] call. + * Transformation that happens during [deserialize] call. * Does nothing by default. */ protected open fun writeTransform(element: JsonElement): JsonElement = element diff --git a/runtime/commonMain/src/kotlinx/serialization/json/internal/ContextValidator.kt b/runtime/commonMain/src/kotlinx/serialization/json/internal/ContextValidator.kt index 917dce619f..bd66f44be5 100644 --- a/runtime/commonMain/src/kotlinx/serialization/json/internal/ContextValidator.kt +++ b/runtime/commonMain/src/kotlinx/serialization/json/internal/ContextValidator.kt @@ -31,4 +31,11 @@ internal class ContextValidator(private val discriminator: String) : SerialModul } } } + + override fun defaultPolymorphic( + baseClass: KClass, + defaultSerializerProvider: (className: String) -> DeserializationStrategy? + ) { + // Nothing here + } } diff --git a/runtime/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt b/runtime/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt index 212a54dc5e..c14b3a2075 100644 --- a/runtime/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt +++ b/runtime/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt @@ -6,6 +6,7 @@ package kotlinx.serialization.modules import kotlinx.serialization.* import kotlinx.serialization.internal.* +import kotlinx.serialization.json.* import kotlin.reflect.* /** @@ -20,6 +21,7 @@ public class PolymorphicModuleBuilder internal constructor( private val baseSerializer: KSerializer? = null ) { private val subclasses: MutableList, KSerializer>> = mutableListOf() + private var defaultSerializerProvider: ((String) -> DeserializationStrategy)? = null /** * Adds a [subclass] [serializer] to the resulting module under the initial [baseClass]. @@ -59,6 +61,21 @@ public class PolymorphicModuleBuilder internal constructor( @ImplicitReflectionSerializer public inline fun subclass(): Unit = addSubclass(T::class, serializer()) + /** + * Registers serializer provider that will be invoked if no polymorphic serializer is present. + * [defaultSerializerProvider] can be stateful and lookup a serializer for the missing type dynamically. + * + * Typically, if the class is not registered in advance, it is not possible to know the structure of the unknown + * type and have a precise serializer, so the default serializer has limited capabilities. + * To have a structural access to the unknown data, it is recommended to use [JsonTransformingSerializer] subclass. + */ + public fun default(defaultSerializerProvider: (className: String) -> DeserializationStrategy?) { + require(this.defaultSerializerProvider == null) { + "Default serializer provider is already registered for class $baseClass: ${this.defaultSerializerProvider}" + } + this.defaultSerializerProvider = defaultSerializerProvider + } + /** * @see addSubclass */ @@ -74,6 +91,10 @@ public class PolymorphicModuleBuilder internal constructor( serializer.cast() ) } + + if (defaultSerializerProvider != null) { + builder.registerDefaultPolymorphicSerializer(baseClass, defaultSerializerProvider!!, false) + } } /** diff --git a/runtime/commonMain/src/kotlinx/serialization/modules/SerialModule.kt b/runtime/commonMain/src/kotlinx/serialization/modules/SerialModule.kt index d96e4eaa7a..98fa4a198b 100644 --- a/runtime/commonMain/src/kotlinx/serialization/modules/SerialModule.kt +++ b/runtime/commonMain/src/kotlinx/serialization/modules/SerialModule.kt @@ -1,54 +1,62 @@ /* - * Copyright 2017-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -@file:Suppress("UNCHECKED_CAST", "RedundantVisibilityModifier") - package kotlinx.serialization.modules import kotlinx.serialization.* +import kotlinx.serialization.internal.* import kotlin.reflect.* /** * [SerialModule] is a collection of serializers used by [ContextSerializer] and [PolymorphicSerializer] * to override or provide serializers at the runtime, whereas at the compile-time they provided by the serialization plugin. + * It can be considered as a map where serializers can be found using their statically known KClasses. * - * It can be considered as a map where serializers are found using statically known KClasses. - * - * To enable runtime resolution of serializers, one of the special annotations must be used on target types - * and a serial module with serializers should be used during construction of [SerialFormat]. + * To enable runtime serializers resolution, one of the special annotations must be used on target types + * ([Polymorphic] or [ContextualSerialization]), and a serial module with serializers should be used during construction of [SerialFormat]. * * @see ContextualSerialization * @see Polymorphic */ -public interface SerialModule { +public sealed class SerialModule { /** * Returns a contextual serializer associated with a given [kclass]. * This method is used in context-sensitive operations on a property marked with [ContextualSerialization] by a [ContextSerializer] */ - public fun getContextual(kclass: KClass): KSerializer? + public abstract fun getContextual(kclass: KClass): KSerializer? /** * Returns a polymorphic serializer registered for a class of the given [value] in the scope of [baseClass]. */ - public fun getPolymorphic(baseClass: KClass, value: T): KSerializer? + public abstract fun getPolymorphic(baseClass: KClass, value: T): KSerializer? /** * Returns a polymorphic serializer registered for for a [serializedClassName] in the scope of [baseClass]. */ - public fun getPolymorphic(baseClass: KClass, serializedClassName: String): KSerializer? + public abstract fun getPolymorphic(baseClass: KClass, serializedClassName: String): KSerializer? + + /** + * Returns a default polymorphic serializer registered for a [serializedClassName] in the scope of [baseClass]. + * In contrast with [getPolymorphic], module user registers a factory function for default serializers that can + * provide a serializer even for an unknown in advance [serializedClassName]. + */ + public abstract fun getDefaultPolymorphic( + baseClass: KClass, + serializedClassName: String + ): DeserializationStrategy? /** * Copies contents of this module to the given [collector]. */ - public fun dumpTo(collector: SerialModuleCollector) + public abstract fun dumpTo(collector: SerialModuleCollector) } /** * A [SerialModule] which is empty and always returns `null`. */ -public object EmptyModule : SerialModule { +public object EmptyModule : SerialModule() { public override fun getContextual(kclass: KClass): KSerializer? = null public override fun getPolymorphic(baseClass: KClass, value: T): KSerializer? = null public override fun getPolymorphic( @@ -56,5 +64,77 @@ public object EmptyModule : SerialModule { serializedClassName: String ): KSerializer? = null + override fun getDefaultPolymorphic( + baseClass: KClass, + serializedClassName: String + ): DeserializationStrategy? = null + public override fun dumpTo(collector: SerialModuleCollector) = Unit } + +internal typealias PolymorphicProvider = (className: String) -> DeserializationStrategy? + +/** + * A default implementation of [SerialModule] + * which uses hash maps to store serializers associated with KClasses. + */ +@Suppress("UNCHECKED_CAST") +internal class SerialModuleImpl( + private val class2Serializer: Map, KSerializer<*>>, + private val polyBase2Serializers: Map, Map, KSerializer<*>>>, + private val polyBase2NamedSerializers: Map, Map>>, + private val polyBase2DefaultProvider: Map, PolymorphicProvider<*>> +) : SerialModule() { + + override fun getPolymorphic(baseClass: KClass, value: T): KSerializer? { + if (!value.isInstanceOf(baseClass)) return null + val custom = polyBase2Serializers[baseClass]?.get(value::class) as? KSerializer + if (custom != null) return custom + if (baseClass == Any::class) { + val serializer = StandardSubtypesOfAny.getSubclassSerializer(value) + return serializer as? KSerializer + } + return null + } + + override fun getDefaultPolymorphic( + baseClass: KClass, + serializedClassName: String + ): DeserializationStrategy? = + (polyBase2DefaultProvider[baseClass] as? PolymorphicProvider)?.invoke(serializedClassName) + + override fun getPolymorphic(baseClass: KClass, serializedClassName: String): KSerializer? { + val standardPolymorphic = + if (baseClass == Any::class) StandardSubtypesOfAny.getDefaultDeserializer(serializedClassName) + else null + + if (standardPolymorphic != null) return standardPolymorphic as KSerializer + return polyBase2NamedSerializers[baseClass]?.get(serializedClassName) as? KSerializer + } + + override fun getContextual(kclass: KClass): KSerializer? = + class2Serializer[kclass] as? KSerializer + + override fun dumpTo(collector: SerialModuleCollector) { + class2Serializer.forEach { (kclass, serial) -> + collector.contextual( + kclass as KClass, + serial.cast() + ) + } + + polyBase2Serializers.forEach { (baseClass, classMap) -> + classMap.forEach { (actualClass, serializer) -> + collector.polymorphic( + baseClass as KClass, + actualClass as KClass, + serializer.cast() + ) + } + } + + polyBase2DefaultProvider.forEach { (baseClass, provider) -> + collector.defaultPolymorphic(baseClass as KClass, provider as (PolymorphicProvider)) + } + } +} diff --git a/runtime/commonMain/src/kotlinx/serialization/modules/SerialModuleBuilders.kt b/runtime/commonMain/src/kotlinx/serialization/modules/SerialModuleBuilders.kt index a8d23a15f2..8f995b676d 100644 --- a/runtime/commonMain/src/kotlinx/serialization/modules/SerialModuleBuilders.kt +++ b/runtime/commonMain/src/kotlinx/serialization/modules/SerialModuleBuilders.kt @@ -57,12 +57,15 @@ public class SerializersModuleBuilder internal constructor() : SerialModuleColle private val class2Serializer: MutableMap, KSerializer<*>> = hashMapOf() private val polyBase2Serializers: MutableMap, MutableMap, KSerializer<*>>> = hashMapOf() private val polyBase2NamedSerializers: MutableMap, MutableMap>> = hashMapOf() + private val polyBase2DefaultProvider: MutableMap, PolymorphicProvider<*>> = hashMapOf() + /** * Adds [serializer] associated with given [kClass] for contextual serialization. * Throws [SerializationException] if a module already has serializer associated with a [kClass]. * To overwrite an already registered serializer, [SerialModule.overwriteWith] can be used. */ - public override fun contextual(kClass: KClass, serializer: KSerializer) = registerSerializer(kClass, serializer) + public override fun contextual(kClass: KClass, serializer: KSerializer) = + registerSerializer(kClass, serializer) /** * Adds [serializer][actualSerializer] associated with given [actualClass] in the scope of [baseClass] for polymorphic serialization. @@ -77,6 +80,13 @@ public class SerializersModuleBuilder internal constructor() : SerialModuleColle registerPolymorphicSerializer(baseClass, actualClass, actualSerializer) } + public override fun defaultPolymorphic( + baseClass: KClass, + defaultSerializerProvider: (className: String) -> DeserializationStrategy? + ) { + registerDefaultPolymorphicSerializer(baseClass, defaultSerializerProvider, false) + } + /** * Copies contents of [other] module into current builder. */ @@ -175,6 +185,19 @@ public class SerializersModuleBuilder internal constructor() : SerialModuleColle class2Serializer[forClass] = serializer } + @JvmName("registerDefaultPolymorphicSerializer") // Don't mangle method name for prettier stack traces + internal fun registerDefaultPolymorphicSerializer( + baseClass: KClass, + defaultSerializerProvider: (className: String) -> DeserializationStrategy?, + allowOverwrite: Boolean + ) { + val previous = polyBase2DefaultProvider[baseClass] + if (previous != null && previous != defaultSerializerProvider && !allowOverwrite) { + throw IllegalArgumentException("Default serializers provider for class $baseClass is already registered: $previous") + } + polyBase2DefaultProvider[baseClass] = defaultSerializerProvider + } + @JvmName("registerPolymorphicSerializer") // Don't mangle method name for prettier stack traces internal fun registerPolymorphicSerializer( baseClass: KClass, @@ -219,5 +242,6 @@ public class SerializersModuleBuilder internal constructor() : SerialModuleColle names[name] = concreteSerializer } - internal fun build(): SerialModule = SerialModuleImpl(class2Serializer, polyBase2Serializers, polyBase2NamedSerializers) + internal fun build(): SerialModule = + SerialModuleImpl(class2Serializer, polyBase2Serializers, polyBase2NamedSerializers, polyBase2DefaultProvider) } diff --git a/runtime/commonMain/src/kotlinx/serialization/modules/SerialModuleCollector.kt b/runtime/commonMain/src/kotlinx/serialization/modules/SerialModuleCollector.kt index 74a1f25b4d..ac2fc3bedb 100644 --- a/runtime/commonMain/src/kotlinx/serialization/modules/SerialModuleCollector.kt +++ b/runtime/commonMain/src/kotlinx/serialization/modules/SerialModuleCollector.kt @@ -28,6 +28,14 @@ public interface SerialModuleCollector { actualClass: KClass, actualSerializer: KSerializer ) + + /** + * Accept a default serializer provider, associated with the [baseClass] for polymorphic serialization. + */ + public fun defaultPolymorphic( + baseClass: KClass, + defaultSerializerProvider: (className: String) -> DeserializationStrategy? + ) } /** diff --git a/runtime/commonMain/src/kotlinx/serialization/modules/SerialModuleExtensions.kt b/runtime/commonMain/src/kotlinx/serialization/modules/SerialModuleExtensions.kt index 3789fccec3..7da7712c9b 100644 --- a/runtime/commonMain/src/kotlinx/serialization/modules/SerialModuleExtensions.kt +++ b/runtime/commonMain/src/kotlinx/serialization/modules/SerialModuleExtensions.kt @@ -59,5 +59,12 @@ public infix fun SerialModule.overwriteWith(other: SerialModule): SerialModule = ) { registerPolymorphicSerializer(baseClass, actualClass, actualSerializer, allowOverwrite = true) } + + override fun defaultPolymorphic( + baseClass: KClass, + defaultSerializerProvider: (className: String) -> DeserializationStrategy? + ) { + registerDefaultPolymorphicSerializer(baseClass, defaultSerializerProvider, allowOverwrite = true) + } }) } diff --git a/runtime/commonMain/src/kotlinx/serialization/modules/SerialModuleImpl.kt b/runtime/commonMain/src/kotlinx/serialization/modules/SerialModuleImpl.kt deleted file mode 100644 index 11d7ca5dca..0000000000 --- a/runtime/commonMain/src/kotlinx/serialization/modules/SerialModuleImpl.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -@file:Suppress("UNCHECKED_CAST") - -package kotlinx.serialization.modules - -import kotlinx.serialization.* -import kotlinx.serialization.internal.* -import kotlin.reflect.* - -/** - * A default implementation of [SerialModule] - * which uses hash maps to store serializers associated with KClasses. - */ -internal class SerialModuleImpl( - private val class2Serializer: Map, KSerializer<*>>, - private val polyBase2Serializers: Map, Map, KSerializer<*>>>, - private val polyBase2NamedSerializers: Map, Map>>) : SerialModule { - - override fun getPolymorphic(baseClass: KClass, value: T): KSerializer? { - if (!value.isInstanceOf(baseClass)) return null - val custom = polyBase2Serializers[baseClass]?.get(value::class) as? KSerializer - if (custom != null) return custom - if (baseClass == Any::class) { - val serializer = StandardSubtypesOfAny.getSubclassSerializer(value) - return serializer as? KSerializer - } - return null - } - - override fun getPolymorphic(baseClass: KClass, serializedClassName: String): KSerializer? { - val standardPolymorphic = - if (baseClass == Any::class) StandardSubtypesOfAny.getDefaultDeserializer(serializedClassName) - else null - - if (standardPolymorphic != null) return standardPolymorphic as KSerializer - return polyBase2NamedSerializers[baseClass]?.get(serializedClassName) as? KSerializer - } - - override fun getContextual(kclass: KClass): KSerializer? = class2Serializer[kclass] as? KSerializer - - override fun dumpTo(collector: SerialModuleCollector) { - class2Serializer.forEach { (kclass, serial) -> - collector.contextual( - kclass as KClass, - serial.cast() - ) - } - - polyBase2Serializers.forEach { (baseClass, classMap) -> - classMap.forEach { (actualClass, serializer) -> - collector.polymorphic( - baseClass as KClass, - actualClass as KClass, - serializer.cast() - ) - } - } - } -} diff --git a/runtime/commonTest/src/kotlinx/serialization/PolymorphismTestData.kt b/runtime/commonTest/src/kotlinx/serialization/PolymorphismTestData.kt index 5193c886a9..b2bfee5caa 100644 --- a/runtime/commonTest/src/kotlinx/serialization/PolymorphismTestData.kt +++ b/runtime/commonTest/src/kotlinx/serialization/PolymorphismTestData.kt @@ -4,6 +4,7 @@ package kotlinx.serialization +import kotlinx.serialization.json.* import kotlinx.serialization.modules.SerializersModule @Serializable @@ -19,16 +20,17 @@ open class PolyBase(val id: Int) { override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || this::class != other::class) return false - other as PolyBase - if (id != other.id) return false - return true } } +// TODO sandwwraith moving this class to the corresponding tests breaks runtime in unexpected ways +@Serializable +data class PolyDefault(val json: JsonElement) : PolyBase(-1) + @Serializable data class PolyDerived(val s: String) : PolyBase(1) diff --git a/runtime/commonTest/src/kotlinx/serialization/features/PolymorphismTest.kt b/runtime/commonTest/src/kotlinx/serialization/features/PolymorphismTest.kt index 7cde1092d1..9392d056dc 100644 --- a/runtime/commonTest/src/kotlinx/serialization/features/PolymorphismTest.kt +++ b/runtime/commonTest/src/kotlinx/serialization/features/PolymorphismTest.kt @@ -5,11 +5,11 @@ package kotlinx.serialization.features import kotlinx.serialization.* -import kotlinx.serialization.json.Json +import kotlinx.serialization.json.* import kotlinx.serialization.modules.* import kotlin.test.* -class PolymorphismTest { +class PolymorphismTest : JsonTestBase() { @Serializable data class Wrapper( @@ -27,12 +27,12 @@ class PolymorphismTest { private val json = Json { unquotedPrint = true; useArrayPolymorphism = true; serialModule = module } @Test - fun testInheritanceJson() { + fun testInheritanceJson() = parametrizedTest { useStreaming -> val obj = Wrapper( PolyBase(2), PolyDerived("b") ) - val bytes = json.stringify(Wrapper.serializer(), obj) + val bytes = json.stringify(Wrapper.serializer(), obj, useStreaming) assertEquals( "{polyBase1:[kotlinx.serialization.PolyBase,{id:2}]," + "polyBase2:[kotlinx.serialization.PolyDerived,{id:1,s:b}]}", bytes @@ -40,9 +40,41 @@ class PolymorphismTest { } @Test - fun testSerializeWithExplicitPolymorhpicSerializer() { + fun testSerializeWithExplicitPolymorphicSerializer() = parametrizedTest { useStreaming -> val obj = PolyDerived("b") - val s = json.stringify(PolymorphicSerializer(PolyDerived::class), obj) + val s = json.stringify(PolymorphicSerializer(PolyDerived::class), obj, useStreaming) assertEquals("[kotlinx.serialization.PolyDerived,{id:1,s:b}]", s) } + + object PolyDefaultSerializer : JsonTransformingSerializer(PolyDefault.serializer(), "foo") { + override fun readTransform(element: JsonElement): JsonElement { + return json { + "json" to element + "id" to 42 + } + } + } + + @Test + fun testDefaultSerializer() = parametrizedTest { useStreaming -> + val withDefault = module + SerializersModule { + defaultPolymorphic(PolyBase::class) { name -> + if (name == "foo") { + PolyDefaultSerializer + } else { + null + } + } + } + + val adjustedJson = Json(json.configuration.copy(useArrayPolymorphism = false), withDefault) + val string = """ + {"polyBase1":{"type":"kotlinx.serialization.PolyBase","id":239}, + "polyBase2":{"type":"foo","key":42}}""".trimIndent() + val result = adjustedJson.parse(Wrapper.serializer(), string, useStreaming) + assertEquals(Wrapper(PolyBase(239), PolyDefault(JsonObject(mapOf("key" to JsonPrimitive(42))))), result) + + val replaced = string.replace("foo", "bar") + assertFailsWithMessage("not registered") { adjustedJson.parse(Wrapper.serializer(), replaced, useStreaming) } + } } diff --git a/runtime/commonTest/src/kotlinx/serialization/modules/ModuleBuildersTest.kt b/runtime/commonTest/src/kotlinx/serialization/modules/ModuleBuildersTest.kt index fae6d06f42..192c7d0ae5 100644 --- a/runtime/commonTest/src/kotlinx/serialization/modules/ModuleBuildersTest.kt +++ b/runtime/commonTest/src/kotlinx/serialization/modules/ModuleBuildersTest.kt @@ -281,4 +281,34 @@ class ModuleBuildersTest { assertEquals(delegate2, aggregate.getPolymorphic(Any::class, Unit)) assertEquals(delegate, aggregate.getPolymorphic(Any::class, Unit)) } + + @Test + fun testPolymorphicCollision() { + val m1 = SerializersModule { + polymorphic { + default { _ -> UnitSerializer() } + } + } + + val m2 = SerializersModule { + polymorphic { + default { _ -> UnitSerializer() } + } + } + + assertFailsWith { m1 + m2 } + } + + @Test + fun testNoPolymorphicCollision() { + val defaultSerializerProvider = { _: String -> UnitSerializer() } + val m1 = SerializersModule { + polymorphic { + default(defaultSerializerProvider) + } + } + + val m2 = m1 + m1 + assertEquals(UnitSerializer(), m2.getDefaultPolymorphic(Any::class, "foo")) + } }