diff --git a/core/api/kotlinx-serialization-core.api b/core/api/kotlinx-serialization-core.api index 0cec51e3f..9159db420 100644 --- a/core/api/kotlinx-serialization-core.api +++ b/core/api/kotlinx-serialization-core.api @@ -1177,6 +1177,7 @@ public final class kotlinx/serialization/modules/PolymorphicModuleBuilder { public fun (Lkotlin/reflect/KClass;Lkotlinx/serialization/KSerializer;)V public final fun buildTo (Lkotlinx/serialization/modules/SerializersModuleBuilder;)V public final fun default (Lkotlin/jvm/functions/Function1;)V + public final fun defaultDeserializer (Lkotlin/jvm/functions/Function1;)V public final fun subclass (Lkotlin/reflect/KClass;Lkotlinx/serialization/KSerializer;)V } @@ -1197,6 +1198,8 @@ public final class kotlinx/serialization/modules/SerializersModuleBuilder : kotl public final fun include (Lkotlinx/serialization/modules/SerializersModule;)V public fun polymorphic (Lkotlin/reflect/KClass;Lkotlin/reflect/KClass;Lkotlinx/serialization/KSerializer;)V public fun polymorphicDefault (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V + public fun polymorphicDefaultDeserializer (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V + public fun polymorphicDefaultSerializer (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V } public final class kotlinx/serialization/modules/SerializersModuleBuildersKt { @@ -1211,10 +1214,13 @@ public abstract interface class kotlinx/serialization/modules/SerializersModuleC public abstract fun contextual (Lkotlin/reflect/KClass;Lkotlinx/serialization/KSerializer;)V public abstract fun polymorphic (Lkotlin/reflect/KClass;Lkotlin/reflect/KClass;Lkotlinx/serialization/KSerializer;)V public abstract fun polymorphicDefault (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V + public abstract fun polymorphicDefaultDeserializer (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V + public abstract fun polymorphicDefaultSerializer (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V } public final class kotlinx/serialization/modules/SerializersModuleCollector$DefaultImpls { public static fun contextual (Lkotlinx/serialization/modules/SerializersModuleCollector;Lkotlin/reflect/KClass;Lkotlinx/serialization/KSerializer;)V + public static fun polymorphicDefault (Lkotlinx/serialization/modules/SerializersModuleCollector;Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V } public final class kotlinx/serialization/modules/SerializersModuleKt { diff --git a/core/commonMain/src/kotlinx/serialization/descriptors/ContextAware.kt b/core/commonMain/src/kotlinx/serialization/descriptors/ContextAware.kt index f05a5c558..4a6367f48 100644 --- a/core/commonMain/src/kotlinx/serialization/descriptors/ContextAware.kt +++ b/core/commonMain/src/kotlinx/serialization/descriptors/ContextAware.kt @@ -63,7 +63,8 @@ public fun SerializersModule.getContextualDescriptor(descriptor: SerialDescripto /** * Retrieves a collection of descriptors which serializers are registered for polymorphic serialization in [this] * with base class equal to [descriptor]'s [SerialDescriptor.capturedKClass]. - * This method does not retrieve serializers registered with [PolymorphicModuleBuilder.default]. + * This method does not retrieve serializers registered with [PolymorphicModuleBuilder.defaultDeserializer] + * or [PolymorphicModuleBuilder.defaultSerializer]. * * @see SerializersModule.getPolymorphic * @see SerializersModuleBuilder.polymorphic diff --git a/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt b/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt index d19dcd6c2..31ce45740 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt @@ -20,7 +20,8 @@ public class PolymorphicModuleBuilder @PublishedApi internal cons private val baseSerializer: KSerializer? = null ) { private val subclasses: MutableList, KSerializer>> = mutableListOf() - private var defaultSerializerProvider: ((String?) -> DeserializationStrategy?)? = null + private var defaultSerializerProvider: ((Base) -> SerializationStrategy?)? = null + private var defaultDeserializerProvider: ((String?) -> DeserializationStrategy?)? = null /** * Registers a [subclass] [serializer] in the resulting module under the [base class][Base]. @@ -31,24 +32,51 @@ public class PolymorphicModuleBuilder @PublishedApi internal cons /** * Adds a default serializers provider associated with the given [baseClass] to the resulting module. + * [defaultDeserializerProvider] is invoked when no polymorphic serializers associated with the `className` + * were found. `className` could be `null` for formats that support nullable class discriminators + * (currently only [Json] with [useArrayPolymorphism][JsonBuilder.useArrayPolymorphism] set to `false`) + * + * [defaultDeserializerProvider] 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] + * or [JsonContentPolymorphicSerializer] classes. + * + * Default deserializers provider affects only deserialization process. + */ + @ExperimentalSerializationApi + public fun defaultDeserializer(defaultDeserializerProvider: (className: String?) -> DeserializationStrategy?) { + require(this.defaultDeserializerProvider == null) { + "Default deserializer provider is already registered for class $baseClass: ${this.defaultDeserializerProvider}" + } + this.defaultDeserializerProvider = defaultDeserializerProvider + } + + /** + * Adds a default deserializers provider associated with the given [baseClass] to the resulting module. * [defaultSerializerProvider] is invoked when no polymorphic serializers associated with the `className` * were found. `className` could be `null` for formats that support nullable class discriminators * (currently only [Json] with [useArrayPolymorphism][JsonBuilder.useArrayPolymorphism] set to `false`) * * [defaultSerializerProvider] can be stateful and lookup a serializer for the missing type dynamically. * + * [defaultSerializerProvider] is named as such for backwards compatibility reasons; it provides deserializers. + * * 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] * or [JsonContentPolymorphicSerializer] classes. * - * Default serializers provider affects only deserialization process. + * Default deserializers provider affects only deserialization process. To affect serialization process, use + * [SerializersModuleBuilder.polymorphicDefaultSerializer]. + * + * @see defaultDeserializer */ + @OptIn(ExperimentalSerializationApi::class) + // TODO: deprecate in 1.4 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 + defaultDeserializer(defaultSerializerProvider) } @Suppress("UNCHECKED_CAST") @@ -63,9 +91,14 @@ public class PolymorphicModuleBuilder @PublishedApi internal cons ) } - val default = defaultSerializerProvider - if (default != null) { - builder.registerDefaultPolymorphicSerializer(baseClass, default, false) + val defaultSerializer = defaultSerializerProvider + if (defaultSerializer != null) { + builder.registerDefaultPolymorphicSerializer(baseClass, defaultSerializer, false) + } + + val defaultDeserializer = defaultDeserializerProvider + if (defaultDeserializer != null) { + builder.registerDefaultPolymorphicDeserializer(baseClass, defaultDeserializer, false) } } } diff --git a/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt b/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt index 5d9211731..86f66d7af 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt @@ -71,7 +71,7 @@ public sealed class SerializersModule { */ @SharedImmutable @ExperimentalSerializationApi -public val EmptySerializersModule: SerializersModule = SerialModuleImpl(emptyMap(), emptyMap(), emptyMap(), emptyMap()) +public val EmptySerializersModule: SerializersModule = SerialModuleImpl(emptyMap(), emptyMap(), emptyMap(), emptyMap(), emptyMap()) /** * Returns a combination of two serial modules @@ -113,12 +113,19 @@ public infix fun SerializersModule.overwriteWith(other: SerializersModule): Seri registerPolymorphicSerializer(baseClass, actualClass, actualSerializer, allowOverwrite = true) } - override fun polymorphicDefault( + override fun polymorphicDefaultSerializer( baseClass: KClass, - defaultSerializerProvider: (className: String?) -> DeserializationStrategy? + defaultSerializerProvider: (value: Base) -> SerializationStrategy? ) { registerDefaultPolymorphicSerializer(baseClass, defaultSerializerProvider, allowOverwrite = true) } + + override fun polymorphicDefaultDeserializer( + baseClass: KClass, + defaultDeserializerProvider: (className: String?) -> DeserializationStrategy? + ) { + registerDefaultPolymorphicDeserializer(baseClass, defaultDeserializerProvider, allowOverwrite = true) + } }) } @@ -133,13 +140,18 @@ public infix fun SerializersModule.overwriteWith(other: SerializersModule): Seri internal class SerialModuleImpl( private val class2ContextualFactory: Map, ContextualProvider>, @JvmField val polyBase2Serializers: Map, Map, KSerializer<*>>>, + private val polyBase2DefaultSerializerProvider: Map, PolymorphicSerializerProvider<*>>, private val polyBase2NamedSerializers: Map, Map>>, - private val polyBase2DefaultProvider: Map, PolymorphicProvider<*>> + private val polyBase2DefaultDeserializerProvider: Map, PolymorphicDeserializerProvider<*>> ) : SerializersModule() { override fun getPolymorphic(baseClass: KClass, value: T): SerializationStrategy? { if (!value.isInstanceOf(baseClass)) return null - return polyBase2Serializers[baseClass]?.get(value::class) as? SerializationStrategy + // Registered + val registered = polyBase2Serializers[baseClass]?.get(value::class) as? SerializationStrategy + if (registered != null) return registered + // Default + return (polyBase2DefaultSerializerProvider[baseClass] as? PolymorphicSerializerProvider)?.invoke(value) } override fun getPolymorphic(baseClass: KClass, serializedClassName: String?): DeserializationStrategy? { @@ -147,7 +159,7 @@ internal class SerialModuleImpl( val registered = polyBase2NamedSerializers[baseClass]?.get(serializedClassName) as? KSerializer if (registered != null) return registered // Default - return (polyBase2DefaultProvider[baseClass] as? PolymorphicProvider)?.invoke(serializedClassName) + return (polyBase2DefaultDeserializerProvider[baseClass] as? PolymorphicDeserializerProvider)?.invoke(serializedClassName) } override fun getContextual(kClass: KClass, typeArgumentsSerializers: List>): KSerializer? { @@ -175,13 +187,18 @@ internal class SerialModuleImpl( } } - polyBase2DefaultProvider.forEach { (baseClass, provider) -> - collector.polymorphicDefault(baseClass as KClass, provider as (PolymorphicProvider)) + polyBase2DefaultSerializerProvider.forEach { (baseClass, provider) -> + collector.polymorphicDefaultSerializer(baseClass as KClass, provider as (PolymorphicSerializerProvider)) + } + + polyBase2DefaultDeserializerProvider.forEach { (baseClass, provider) -> + collector.polymorphicDefaultDeserializer(baseClass as KClass, provider as (PolymorphicDeserializerProvider)) } } } -internal typealias PolymorphicProvider = (className: String?) -> DeserializationStrategy? +internal typealias PolymorphicDeserializerProvider = (className: String?) -> DeserializationStrategy? +internal typealias PolymorphicSerializerProvider = (value: Base) -> SerializationStrategy? /** This class is needed to support re-registering the same static (argless) serializers: * diff --git a/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleBuilders.kt b/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleBuilders.kt index f81f27fc1..530f42647 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleBuilders.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleBuilders.kt @@ -40,8 +40,9 @@ public inline fun SerializersModule(builderAction: SerializersModuleBuilder.() - public class SerializersModuleBuilder @PublishedApi internal constructor() : SerializersModuleCollector { private val class2ContextualProvider: MutableMap, ContextualProvider> = hashMapOf() private val polyBase2Serializers: MutableMap, MutableMap, KSerializer<*>>> = hashMapOf() + private val polyBase2DefaultSerializerProvider: MutableMap, PolymorphicSerializerProvider<*>> = hashMapOf() private val polyBase2NamedSerializers: MutableMap, MutableMap>> = hashMapOf() - private val polyBase2DefaultProvider: MutableMap, PolymorphicProvider<*>> = hashMapOf() + private val polyBase2DefaultDeserializerProvider: MutableMap, PolymorphicDeserializerProvider<*>> = hashMapOf() /** * Adds [serializer] associated with given [kClass] for contextual serialization. @@ -91,19 +92,36 @@ public class SerializersModuleBuilder @PublishedApi internal constructor() : Ser /** * Adds a default serializers provider associated with the given [baseClass] to the resulting module. - * [defaultSerializerProvider] is invoked when no polymorphic serializers associated with the `className` - * were found. `className` could be `null` for formats that support nullable class discriminators - * (currently only `Json` with `useArrayPolymorphism` set to `false`) + * [defaultSerializerProvider] is invoked when no polymorphic serializers for `value` were found. * - * @see PolymorphicModuleBuilder.default + * This will not affect deserialization. */ - public override fun polymorphicDefault( + @ExperimentalSerializationApi + public override fun polymorphicDefaultSerializer( baseClass: KClass, - defaultSerializerProvider: (className: String?) -> DeserializationStrategy? + defaultSerializerProvider: (value: Base) -> SerializationStrategy? ) { registerDefaultPolymorphicSerializer(baseClass, defaultSerializerProvider, false) } + /** + * Adds a default deserializers provider associated with the given [baseClass] to the resulting module. + * [defaultDeserializerProvider] is invoked when no polymorphic serializers associated with the `className` + * were found. `className` could be `null` for formats that support nullable class discriminators + * (currently only `Json` with `useArrayPolymorphism` set to `false`). + * + * This will not affect serialization. + * + * @see PolymorphicModuleBuilder.defaultDeserializer + */ + @ExperimentalSerializationApi + public override fun polymorphicDefaultDeserializer( + baseClass: KClass, + defaultDeserializerProvider: (className: String?) -> DeserializationStrategy? + ) { + registerDefaultPolymorphicDeserializer(baseClass, defaultDeserializerProvider, false) + } + /** * Copies the content of [module] module into the current builder. */ @@ -132,14 +150,27 @@ public class SerializersModuleBuilder @PublishedApi internal constructor() : Ser @JvmName("registerDefaultPolymorphicSerializer") // Don't mangle method name for prettier stack traces internal fun registerDefaultPolymorphicSerializer( baseClass: KClass, - defaultSerializerProvider: (className: String?) -> DeserializationStrategy?, + defaultSerializerProvider: (value: Base) -> SerializationStrategy?, allowOverwrite: Boolean ) { - val previous = polyBase2DefaultProvider[baseClass] + val previous = polyBase2DefaultDeserializerProvider[baseClass] if (previous != null && previous != defaultSerializerProvider && !allowOverwrite) { throw IllegalArgumentException("Default serializers provider for class $baseClass is already registered: $previous") } - polyBase2DefaultProvider[baseClass] = defaultSerializerProvider + polyBase2DefaultSerializerProvider[baseClass] = defaultSerializerProvider + } + + @JvmName("registerDefaultPolymorphicDeserializer") // Don't mangle method name for prettier stack traces + internal fun registerDefaultPolymorphicDeserializer( + baseClass: KClass, + defaultDeserializerProvider: (className: String?) -> DeserializationStrategy?, + allowOverwrite: Boolean + ) { + val previous = polyBase2DefaultDeserializerProvider[baseClass] + if (previous != null && previous != defaultDeserializerProvider && !allowOverwrite) { + throw IllegalArgumentException("Default deserializers provider for class $baseClass is already registered: $previous") + } + polyBase2DefaultDeserializerProvider[baseClass] = defaultDeserializerProvider } @JvmName("registerPolymorphicSerializer") // Don't mangle method name for prettier stack traces @@ -188,7 +219,7 @@ public class SerializersModuleBuilder @PublishedApi internal constructor() : Ser @PublishedApi internal fun build(): SerializersModule = - SerialModuleImpl(class2ContextualProvider, polyBase2Serializers, polyBase2NamedSerializers, polyBase2DefaultProvider) + SerialModuleImpl(class2ContextualProvider, polyBase2Serializers, polyBase2DefaultSerializerProvider, polyBase2NamedSerializers, polyBase2DefaultDeserializerProvider) } /** diff --git a/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleCollector.kt b/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleCollector.kt index 10ebc3b1f..c4af77f85 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleCollector.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleCollector.kt @@ -48,11 +48,44 @@ public interface SerializersModuleCollector { /** * Accept a default serializer provider, associated with the [baseClass] for polymorphic serialization. * - * @see SerializersModuleBuilder.polymorphicDefault - * @see PolymorphicModuleBuilder.default + * This will not affect deserialization. + * + * @see SerializersModuleBuilder.polymorphicDefaultSerializer + * @see PolymorphicModuleBuilder.defaultSerializer */ - public fun polymorphicDefault( + @ExperimentalSerializationApi + public fun polymorphicDefaultSerializer( + baseClass: KClass, + defaultSerializerProvider: (value: Base) -> SerializationStrategy? + ) + + /** + * Accept a default deserializer provider, associated with the [baseClass] for polymorphic deserialization. + * + * This will not affect serialization. + * + * @see SerializersModuleBuilder.polymorphicDefaultDeserializer + * @see PolymorphicModuleBuilder.defaultDeserializer + */ + @ExperimentalSerializationApi + public fun polymorphicDefaultDeserializer( baseClass: KClass, - defaultSerializerProvider: (className: String?) -> DeserializationStrategy? + defaultDeserializerProvider: (className: String?) -> DeserializationStrategy? ) + + /** + * Accept a default deserializer provider, associated with the [baseClass] for polymorphic deserialization. + * + * This will not affect serialization. + * + * @see SerializersModuleBuilder.polymorphicDefaultDeserializer + * @see PolymorphicModuleBuilder.defaultDeserializer + */ + // TODO: deprecate in 1.4 + public fun polymorphicDefault( + baseClass: KClass, + defaultDeserializerProvider: (className: String?) -> DeserializationStrategy? + ) { + polymorphicDefaultDeserializer(baseClass, defaultDeserializerProvider) + } } diff --git a/core/commonTest/src/kotlinx/serialization/modules/ModuleBuildersTest.kt b/core/commonTest/src/kotlinx/serialization/modules/ModuleBuildersTest.kt index 708cc25e4..edbd9d6f4 100644 --- a/core/commonTest/src/kotlinx/serialization/modules/ModuleBuildersTest.kt +++ b/core/commonTest/src/kotlinx/serialization/modules/ModuleBuildersTest.kt @@ -279,13 +279,13 @@ class ModuleBuildersTest { fun testPolymorphicCollision() { val m1 = SerializersModule { polymorphic(Any::class) { - default { _ -> Unit.serializer() } + defaultDeserializer { _ -> Unit.serializer() } } } val m2 = SerializersModule { polymorphic(Any::class) { - default { _ -> Unit.serializer() } + defaultDeserializer { _ -> Unit.serializer() } } } @@ -297,7 +297,7 @@ class ModuleBuildersTest { val defaultSerializerProvider = { _: String? -> Unit.serializer() } val m1 = SerializersModule { polymorphic(Any::class) { - default(defaultSerializerProvider) + defaultDeserializer(defaultSerializerProvider) } } diff --git a/docs/polymorphism.md b/docs/polymorphism.md index c54bcb820..e7b76965a 100644 --- a/docs/polymorphism.md +++ b/docs/polymorphism.md @@ -26,6 +26,7 @@ In this chapter we'll see how Kotlin Serialization deals with polymorphic class * [Polymorphism and generic classes](#polymorphism-and-generic-classes) * [Merging library serializers modules](#merging-library-serializers-modules) * [Default polymorphic type handler for deserialization](#default-polymorphic-type-handler-for-deserialization) + * [Default polymorphic type handler for serialization](#default-polymorphic-type-handler-for-serialization) @@ -854,7 +855,7 @@ data class BasicProject(override val name: String, val type: String): Project() data class OwnedProject(override val name: String, val owner: String) : Project() ``` -We register a default handler using the [`default`][PolymorphicModuleBuilder.default] function in +We register a default deserializer handler using the [`defaultDeserializer`][PolymorphicModuleBuilder.defaultDeserializer] function in the [`polymorphic { ... }`][PolymorphicModuleBuilder] DSL that defines a strategy which maps the `type` string from the input to the [deserialization strategy][DeserializationStrategy]. In the below example we don't use the type, but always return the [Plugin-generated serializer](serializers.md#plugin-generated-serializer) @@ -864,7 +865,7 @@ of the `BasicProject` class. val module = SerializersModule { polymorphic(Project::class) { subclass(OwnedProject::class) - default { BasicProject.serializer() } + defaultDeserializer { BasicProject.serializer() } } } ``` @@ -895,11 +896,109 @@ Notice, how `BasicProject` had also captured the specified type key in its `type -We used a plugin-generated serializer as a default serializer, implying that +We used a plugin-generated serializer as a default serializer, implying that the structure of the "unknown" data is known in advance. In a real-world API it's rarely the case. For that purpose a custom, less-structured serializer is needed. You will see the example of such serializer in the future section on [Maintaining custom JSON attributes](json.md#maintaining-custom-json-attributes). +### Default polymorphic type handler for serialization + +Sometimes you need to dynamically choose which serializer to use for a polymorphic type based on the instance, for example if you +don't have access to the full type hierarchy, or if it changes a lot. For this situation, you can register a default serializer. + + + +```kotlin +interface Animal { +} + +interface Cat : Animal { + val catType: String +} + +interface Dog : Animal { + val dogType: String +} + +private class CatImpl : Cat { + override val catType: String = "Tabby" +} + +private class DogImpl : Dog { + override val dogType: String = "Husky" +} + +object AnimalProvider { + fun createCat(): Cat = CatImpl() + fun createDog(): Dog = DogImpl() +} +``` + +We register a default serializer handler using the [`polymorphicDefaultSerializer`][SerializersModuleBuilder.polymorphicDefaultSerializer] function in +the [`SerializersModule { ... }`][SerializersModuleBuilder] DSL that defines a strategy which takes an instance of the base class and +provides a [serialization strategy][SerializationStrategy]. In the below example we use a `when` block to check the type of the +instance, without ever having to refer to the private implementation classes. + +```kotlin +val module = SerializersModule { + polymorphicDefaultSerializer(Animal::class) { instance -> + @Suppress("UNCHECKED_CAST") + when (instance) { + is Cat -> CatSerializer as SerializationStrategy + is Dog -> DogSerializer as SerializationStrategy + else -> null + } + } +} + +object CatSerializer : SerializationStrategy { + override val descriptor = buildClassSerialDescriptor("Cat") { + element("catType") + } + + override fun serialize(encoder: Encoder, value: Cat) { + encoder.encodeStructure(descriptor) { + encodeStringElement(descriptor, 0, value.catType) + } + } +} + +object DogSerializer : SerializationStrategy { + override val descriptor = buildClassSerialDescriptor("Dog") { + element("dogType") + } + + override fun serialize(encoder: Encoder, value: Dog) { + encoder.encodeStructure(descriptor) { + encodeStringElement(descriptor, 0, value.dogType) + } + } +} +``` + +Using this module we can now serialize instances of `Cat` and `Dog`. + +```kotlin +val format = Json { serializersModule = module } + +fun main() { + println(format.encodeToString(AnimalProvider.createCat())) +} +``` + +> You can get the full code [here](../guide/example/example-poly-20.kt) + +```text +{"type":"Cat","catType":"Tabby"} +``` + + + + --- The next chapter covers [JSON features](json.md). @@ -911,6 +1010,7 @@ The next chapter covers [JSON features](json.md). [Serializable]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx-serialization-core/kotlinx.serialization/-serializable/index.html [Polymorphic]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx-serialization-core/kotlinx.serialization/-polymorphic/index.html [DeserializationStrategy]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx-serialization-core/kotlinx.serialization/-deserialization-strategy/index.html +[SerializationStrategy]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx-serialization-core/kotlinx.serialization/-serialization-strategy/index.html [SerializersModule]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx-serialization-core/kotlinx.serialization.modules/-serializers-module/index.html [SerializersModule()]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx-serialization-core/kotlinx.serialization.modules/-serializers-module.html @@ -918,8 +1018,10 @@ The next chapter covers [JSON features](json.md). [subclass]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx-serialization-core/kotlinx.serialization.modules/subclass.html [plus]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx-serialization-core/kotlinx.serialization.modules/plus.html [SerializersModuleBuilder.include]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx-serialization-core/kotlinx.serialization.modules/-serializers-module-builder/include.html -[PolymorphicModuleBuilder.default]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx-serialization-core/kotlinx.serialization.modules/-polymorphic-module-builder/default.html +[PolymorphicModuleBuilder.defaultDeserializer]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx-serialization-core/kotlinx.serialization.modules/-polymorphic-module-builder/default-deserializer.html [PolymorphicModuleBuilder]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx-serialization-core/kotlinx.serialization.modules/-polymorphic-module-builder/index.html +[SerializersModuleBuilder.polymorphicDefaultSerializer]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx-serialization-core/kotlinx.serialization.modules/-serializers-module-builder/polymorphic-default-serializer.html +[SerializersModuleBuilder]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx-serialization-core/kotlinx.serialization.modules/-serializers-module-builder/index.html [Json.encodeToString]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-json/kotlinx-serialization-json/kotlinx.serialization.json/-json/encode-to-string.html diff --git a/docs/serialization-guide.md b/docs/serialization-guide.md index 0563391cd..ef65696e9 100644 --- a/docs/serialization-guide.md +++ b/docs/serialization-guide.md @@ -99,6 +99,7 @@ Once the project is set up, we can start serializing some classes. * [Polymorphism and generic classes](polymorphism.md#polymorphism-and-generic-classes) * [Merging library serializers modules](polymorphism.md#merging-library-serializers-modules) * [Default polymorphic type handler for deserialization](polymorphism.md#default-polymorphic-type-handler-for-deserialization) + * [Default polymorphic type handler for serialization](polymorphism.md#default-polymorphic-type-handler-for-serialization) **Chapter 5.** [JSON Features](json.md) diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/PolymorphismValidator.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/PolymorphismValidator.kt index 04cdb045b..01994f750 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/PolymorphismValidator.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/PolymorphismValidator.kt @@ -74,9 +74,16 @@ internal class PolymorphismValidator( } } - override fun polymorphicDefault( + override fun polymorphicDefaultSerializer( baseClass: KClass, - defaultSerializerProvider: (className: String?) -> DeserializationStrategy? + defaultSerializerProvider: (value: Base) -> SerializationStrategy? + ) { + // Nothing here + } + + override fun polymorphicDefaultDeserializer( + baseClass: KClass, + defaultDeserializerProvider: (className: String?) -> DeserializationStrategy? ) { // Nothing here } diff --git a/formats/json/commonTest/src/kotlinx/serialization/PolymorphismTestData.kt b/formats/json/commonTest/src/kotlinx/serialization/PolymorphismTestData.kt index 5aaf628b7..6d4d42f2a 100644 --- a/formats/json/commonTest/src/kotlinx/serialization/PolymorphismTestData.kt +++ b/formats/json/commonTest/src/kotlinx/serialization/PolymorphismTestData.kt @@ -32,6 +32,8 @@ open class PolyBase(val id: Int) { @Serializable data class PolyDefault(val json: JsonElement) : PolyBase(-1) +class PolyDefaultWithId(id: Int) : PolyBase(id) + @Serializable data class PolyDerived(val s: String) : PolyBase(1) diff --git a/formats/json/commonTest/src/kotlinx/serialization/features/PolymorphismTest.kt b/formats/json/commonTest/src/kotlinx/serialization/features/PolymorphismTest.kt index 253905a3c..d05403b8a 100644 --- a/formats/json/commonTest/src/kotlinx/serialization/features/PolymorphismTest.kt +++ b/formats/json/commonTest/src/kotlinx/serialization/features/PolymorphismTest.kt @@ -5,6 +5,8 @@ package kotlinx.serialization.features import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* import kotlinx.serialization.json.* import kotlinx.serialization.modules.* import kotlinx.serialization.test.* @@ -47,19 +49,43 @@ class PolymorphismTest : JsonTestBase() { assertEquals("""["kotlinx.serialization.PolyDerived",{"id":1,"s":"b"}]""", s) } - object PolyDefaultSerializer : JsonTransformingSerializer(PolyDefault.serializer()) { + object PolyDefaultDeserializer : JsonTransformingSerializer(PolyDefault.serializer()) { override fun transformDeserialize(element: JsonElement): JsonElement = buildJsonObject { put("json", JsonObject(element.jsonObject.filterKeys { it != "type" })) put("id", 42) } } + object EvenDefaultSerializer : SerializationStrategy { + override val descriptor = buildClassSerialDescriptor("even") { + element("parity") + } + + override fun serialize(encoder: Encoder, value: PolyBase) { + encoder.encodeStructure(descriptor) { + encodeStringElement(descriptor, 0, "even") + } + } + } + + object OddDefaultSerializer : SerializationStrategy { + override val descriptor = buildClassSerialDescriptor("odd") { + element("parity") + } + + override fun serialize(encoder: Encoder, value: PolyBase) { + encoder.encodeStructure(descriptor) { + encodeStringElement(descriptor, 0, "odd") + } + } + } + @Test - fun testDefaultSerializer() = parametrizedTest { jsonTestingMode -> + fun testDefaultDeserializer() = parametrizedTest { jsonTestingMode -> val withDefault = module + SerializersModule { - polymorphicDefault(PolyBase::class) { name -> + polymorphicDefaultDeserializer(PolyBase::class) { name -> if (name == "foo") { - PolyDefaultSerializer + PolyDefaultDeserializer } else { null } @@ -78,12 +104,12 @@ class PolymorphismTest : JsonTestBase() { } @Test - fun testDefaultSerializerForMissingDiscriminator() = parametrizedTest { jsonTestingMode -> + fun testDefaultDeserializerForMissingDiscriminator() = parametrizedTest { jsonTestingMode -> val json = Json { serializersModule = module + SerializersModule { - polymorphicDefault(PolyBase::class) { name -> + polymorphicDefaultDeserializer(PolyBase::class) { name -> if (name == null) { - PolyDefaultSerializer + PolyDefaultDeserializer } else { null } @@ -96,4 +122,25 @@ class PolymorphismTest : JsonTestBase() { val result = json.decodeFromString(Wrapper.serializer(), string, jsonTestingMode) assertEquals(Wrapper(PolyBase(239), PolyDefault(JsonObject(mapOf("key" to JsonPrimitive(42))))), result) } + + @Test + fun testDefaultSerializer() = parametrizedTest { jsonTestingMode -> + val json = Json { + serializersModule = module + SerializersModule { + polymorphicDefaultSerializer(PolyBase::class) { value -> + if (value.id % 2 == 0) { + EvenDefaultSerializer + } else { + OddDefaultSerializer + } + } + } + } + val obj = Wrapper( + PolyDefaultWithId(0), + PolyDefaultWithId(1) + ) + val s = json.encodeToString(Wrapper.serializer(), obj, jsonTestingMode) + assertEquals("""{"polyBase1":{"type":"even","parity":"even"},"polyBase2":{"type":"odd","parity":"odd"}}""", s) + } } diff --git a/guide/example/example-poly-19.kt b/guide/example/example-poly-19.kt index b3ce738c6..83677ebe4 100644 --- a/guide/example/example-poly-19.kt +++ b/guide/example/example-poly-19.kt @@ -21,7 +21,7 @@ data class OwnedProject(override val name: String, val owner: String) : Project( val module = SerializersModule { polymorphic(Project::class) { subclass(OwnedProject::class) - default { BasicProject.serializer() } + defaultDeserializer { BasicProject.serializer() } } } diff --git a/guide/example/example-poly-20.kt b/guide/example/example-poly-20.kt new file mode 100644 index 000000000..b597fbeb3 --- /dev/null +++ b/guide/example/example-poly-20.kt @@ -0,0 +1,74 @@ +// This file was automatically generated from polymorphism.md by Knit tool. Do not edit. +package example.examplePoly20 + +import kotlinx.serialization.* +import kotlinx.serialization.json.* + +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* +import kotlinx.serialization.modules.* + +interface Animal { +} + +interface Cat : Animal { + val catType: String +} + +interface Dog : Animal { + val dogType: String +} + +private class CatImpl : Cat { + override val catType: String = "Tabby" +} + +private class DogImpl : Dog { + override val dogType: String = "Husky" +} + +object AnimalProvider { + fun createCat(): Cat = CatImpl() + fun createDog(): Dog = DogImpl() +} + +val module = SerializersModule { + polymorphicDefaultSerializer(Animal::class) { instance -> + @Suppress("UNCHECKED_CAST") + when (instance) { + is Cat -> CatSerializer as SerializationStrategy + is Dog -> DogSerializer as SerializationStrategy + else -> null + } + } +} + +object CatSerializer : SerializationStrategy { + override val descriptor = buildClassSerialDescriptor("Cat") { + element("catType") + } + + override fun serialize(encoder: Encoder, value: Cat) { + encoder.encodeStructure(descriptor) { + encodeStringElement(descriptor, 0, value.catType) + } + } +} + +object DogSerializer : SerializationStrategy { + override val descriptor = buildClassSerialDescriptor("Dog") { + element("dogType") + } + + override fun serialize(encoder: Encoder, value: Dog) { + encoder.encodeStructure(descriptor) { + encodeStringElement(descriptor, 0, value.dogType) + } + } +} + +val format = Json { serializersModule = module } + +fun main() { + println(format.encodeToString(AnimalProvider.createCat())) +} diff --git a/guide/test/PolymorphismTest.kt b/guide/test/PolymorphismTest.kt index ae43ed8d8..e82dd6892 100644 --- a/guide/test/PolymorphismTest.kt +++ b/guide/test/PolymorphismTest.kt @@ -142,4 +142,11 @@ class PolymorphismTest { "[BasicProject(name=example, type=unknown), OwnedProject(name=kotlinx.serialization, owner=kotlin)]" ) } + + @Test + fun testExamplePoly20() { + captureOutput("ExamplePoly20") { example.examplePoly20.main() }.verifyOutputLines( + "{\"type\":\"Cat\",\"catType\":\"Tabby\"}" + ) + } }