diff --git a/core/api/kotlinx-serialization-core.api b/core/api/kotlinx-serialization-core.api index 2ecdf89a57..0b08acb367 100644 --- a/core/api/kotlinx-serialization-core.api +++ b/core/api/kotlinx-serialization-core.api @@ -1336,6 +1336,7 @@ public final class kotlinx/serialization/modules/PolymorphicModuleBuilder { 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 + public final fun subclassesOfSealed (Lkotlinx/serialization/KSerializer;)V } public abstract class kotlinx/serialization/modules/SerializersModule { diff --git a/core/api/kotlinx-serialization-core.klib.api b/core/api/kotlinx-serialization-core.klib.api index 537c74c2db..97bc3b2ad6 100644 --- a/core/api/kotlinx-serialization-core.klib.api +++ b/core/api/kotlinx-serialization-core.klib.api @@ -527,9 +527,11 @@ final class <#A: in kotlin/Any> kotlinx.serialization.modules/PolymorphicModuleB constructor (kotlin.reflect/KClass<#A>, kotlinx.serialization/KSerializer<#A>? = ...) // kotlinx.serialization.modules/PolymorphicModuleBuilder.|(kotlin.reflect.KClass<1:0>;kotlinx.serialization.KSerializer<1:0>?){}[0] final fun <#A1: #A> subclass(kotlin.reflect/KClass<#A1>, kotlinx.serialization/KSerializer<#A1>) // kotlinx.serialization.modules/PolymorphicModuleBuilder.subclass|subclass(kotlin.reflect.KClass<0:0>;kotlinx.serialization.KSerializer<0:0>){0§<1:0>}[0] + final fun <#A1: #A> subclassesOfSealed(kotlinx.serialization/KSerializer<#A1>) // kotlinx.serialization.modules/PolymorphicModuleBuilder.subclassesOfSealed|subclassesOfSealed(kotlinx.serialization.KSerializer<0:0>){0§<1:0>}[0] final fun buildTo(kotlinx.serialization.modules/SerializersModuleBuilder) // kotlinx.serialization.modules/PolymorphicModuleBuilder.buildTo|buildTo(kotlinx.serialization.modules.SerializersModuleBuilder){}[0] final fun default(kotlin/Function1?>) // kotlinx.serialization.modules/PolymorphicModuleBuilder.default|default(kotlin.Function1?>){}[0] final fun defaultDeserializer(kotlin/Function1?>) // kotlinx.serialization.modules/PolymorphicModuleBuilder.defaultDeserializer|defaultDeserializer(kotlin.Function1?>){}[0] + final inline fun <#A1: reified #A> subclassesOfSealed() // kotlinx.serialization.modules/PolymorphicModuleBuilder.subclassesOfSealed|subclassesOfSealed(){0§<1:0>}[0] } final class <#A: kotlin/Any, #B: #A?> kotlinx.serialization.internal/ReferenceArraySerializer : kotlinx.serialization.internal/CollectionLikeSerializer<#B, kotlin/Array<#B>, kotlin.collections/ArrayList<#B>> { // kotlinx.serialization.internal/ReferenceArraySerializer|null[0] @@ -1171,6 +1173,7 @@ final inline fun (kotlinx.serialization.encoding/Encoder).kotlinx.serialization. final inline fun (kotlinx.serialization.encoding/Encoder).kotlinx.serialization.encoding/encodeStructure(kotlinx.serialization.descriptors/SerialDescriptor, crossinline kotlin/Function1) // kotlinx.serialization.encoding/encodeStructure|encodeStructure@kotlinx.serialization.encoding.Encoder(kotlinx.serialization.descriptors.SerialDescriptor;kotlin.Function1){}[0] final inline fun <#A: kotlin/Any, #B: reified #A> (kotlinx.serialization.modules/PolymorphicModuleBuilder<#A>).kotlinx.serialization.modules/subclass(kotlin.reflect/KClass<#B>) // kotlinx.serialization.modules/subclass|subclass@kotlinx.serialization.modules.PolymorphicModuleBuilder<0:0>(kotlin.reflect.KClass<0:1>){0§;1§<0:0>}[0] final inline fun <#A: kotlin/Any, #B: reified #A> (kotlinx.serialization.modules/PolymorphicModuleBuilder<#A>).kotlinx.serialization.modules/subclass(kotlinx.serialization/KSerializer<#B>) // kotlinx.serialization.modules/subclass|subclass@kotlinx.serialization.modules.PolymorphicModuleBuilder<0:0>(kotlinx.serialization.KSerializer<0:1>){0§;1§<0:0>}[0] +final inline fun <#A: kotlin/Any, #B: reified #A> (kotlinx.serialization.modules/PolymorphicModuleBuilder<#A>).kotlinx.serialization.modules/subclassesOfSealed() // kotlinx.serialization.modules/subclassesOfSealed|subclassesOfSealed@kotlinx.serialization.modules.PolymorphicModuleBuilder<0:0>(){0§;1§<0:0>}[0] final inline fun <#A: kotlin/Any> (kotlinx.serialization.modules/SerializersModuleBuilder).kotlinx.serialization.modules/polymorphic(kotlin.reflect/KClass<#A>, kotlinx.serialization/KSerializer<#A>? = ..., kotlin/Function1, kotlin/Unit> = ...) // kotlinx.serialization.modules/polymorphic|polymorphic@kotlinx.serialization.modules.SerializersModuleBuilder(kotlin.reflect.KClass<0:0>;kotlinx.serialization.KSerializer<0:0>?;kotlin.Function1,kotlin.Unit>){0§}[0] final inline fun <#A: kotlin/Any?> (kotlinx.serialization.encoding/Decoder).kotlinx.serialization.encoding/decodeStructure(kotlinx.serialization.descriptors/SerialDescriptor, crossinline kotlin/Function1): #A // kotlinx.serialization.encoding/decodeStructure|decodeStructure@kotlinx.serialization.encoding.Decoder(kotlinx.serialization.descriptors.SerialDescriptor;kotlin.Function1){0§}[0] final inline fun <#A: kotlin/Any?> (kotlinx.serialization.encoding/Encoder).kotlinx.serialization.encoding/encodeCollection(kotlinx.serialization.descriptors/SerialDescriptor, kotlin.collections/Collection<#A>, crossinline kotlin/Function3) // kotlinx.serialization.encoding/encodeCollection|encodeCollection@kotlinx.serialization.encoding.Encoder(kotlinx.serialization.descriptors.SerialDescriptor;kotlin.collections.Collection<0:0>;kotlin.Function3){0§}[0] diff --git a/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt b/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt index 52b7c0544d..74c17f6488 100644 --- a/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt +++ b/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt @@ -115,7 +115,7 @@ public class SealedClassSerializer( } } - private val class2Serializer: Map, KSerializer> + internal val class2Serializer: Map, KSerializer> private val serialName2Serializer: Map> init { diff --git a/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt b/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt index 1b8d431e1a..77cfe58123 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt @@ -5,6 +5,7 @@ package kotlinx.serialization.modules import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* import kotlinx.serialization.internal.* import kotlin.reflect.* @@ -23,6 +24,84 @@ public class PolymorphicModuleBuilder @PublishedApi internal cons private var defaultSerializerProvider: ((Base) -> SerializationStrategy?)? = null private var defaultDeserializerProvider: ((String?) -> DeserializationStrategy?)? = null + + /** + * Registers the child serializers for the sealed type [T] in the resulting module under the [base class][Base]. + * Please note that type `T` has to be sealed and have a standard serializer, if not a runtime error will be + * thrown at registration time. If one of [T]'s subclasses is a sealed serializable class on its own, its + * subclasses are registered recursively as well. If one of [T]'s subclasses is an open polymorphic class + * an [IllegalArgumentException] is thrown. + * + * This function is a convenience function for the version that receives a serializer. + * + * Example: + * ``` + * interface Base + * + * @Serializable + * sealed interface Sub: Base + * + * @Serializable + * class Sub1: Sub + * + * serializersModule { + * polymorphic(Base::class) { + * subclassesOfSealed() + * } + * } + * ``` + */ + @ExperimentalSerializationApi + public inline fun subclassesOfSealed(): Unit = + subclassesOfSealed(serializer()) + + + /** + * Registers the child serializers for the sealed type [T] in the resulting module under the [base class][Base]. + * Please note that type `T` has to be sealed and have a standard serializer, if not a runtime error will be + * thrown at registration time. If one of [T]'s subclasses is a sealed serializable class on its own, its + * subclasses are registered recursively as well. If one of [T]'s subclasses is an open polymorphic class + * an [IllegalArgumentException] is thrown. + * + * Example: + * ```kotlin + * interface Base + * + * @Serializable + * sealed interface Sub: Base + * + * @Serializable + * class Sub1: Sub + * + * serializersModule { + * polymorphic(Base::class) { + * subclassesOfSealed(Sub.serializer()) + * } + * } + * ``` + * + * Note that if Sub1 is itself open polymorphic this is an error. + * + */ + @ExperimentalSerializationApi + public fun subclassesOfSealed(serializer: KSerializer) { + // Note that the parameter type is `KSerializer` as `SealedClassSerializer` is an internal type + // not available to users + require(serializer is SealedClassSerializer) { + "subclassesOfSealed only supports automatic adding of subclasses of sealed types with standard serializers." + } + for ((subsubclass, subserializer) in serializer.class2Serializer.entries) { + // This error would be caught by the Json format in its validation, but this is format specific + require (subserializer.descriptor.kind != PolymorphicKind.OPEN) { + "It is not possible to register subclasses (${serializer.descriptor.serialName}) of sealed types when those subclasses " + + "themselves are (open) polymorphic, as this would represent an incomplete hierarchy." + } + @Suppress("UNCHECKED_CAST") + // We don't know the type here, but it matches if correct in the sealed serializer. + subclass(subsubclass as KClass, subserializer as KSerializer) + } + } + /** * Registers a [subclass] [serializer] in the resulting module under the [base class][Base]. */ @@ -116,3 +195,10 @@ public inline fun PolymorphicModuleBuilder. */ public inline fun PolymorphicModuleBuilder.subclass(clazz: KClass): Unit = subclass(clazz, serializer()) + +/** + * Registers the child serializers for the sealed class [T] in the resulting module under the [base class][Base]. + */ +@ExperimentalSerializationApi +public inline fun PolymorphicModuleBuilder.subclassesOfSealed(): Unit = + subclassesOfSealed(serializer()) diff --git a/docs/polymorphism.md b/docs/polymorphism.md index 9294b67c28..a896fad559 100644 --- a/docs/polymorphism.md +++ b/docs/polymorphism.md @@ -19,6 +19,7 @@ In this chapter we'll see how Kotlin Serialization deals with polymorphic class * [Open polymorphism](#open-polymorphism) * [Registered subclasses](#registered-subclasses) * [Serializing interfaces](#serializing-interfaces) + * [Registering sealed children as subclasses](#registering-sealed-children-as-subclasses) * [Property of an interface type](#property-of-an-interface-type) * [Static parent type lookup for polymorphism](#static-parent-type-lookup-for-polymorphism) * [Explicitly marking polymorphic class properties](#explicitly-marking-polymorphic-class-properties) @@ -414,6 +415,68 @@ fun main() { > Note: On Kotlin/Native, you should use `format.encodeToString(PolymorphicSerializer(Project::class), data))` instead due to limited reflection capabilities. +### Registering sealed children as subclasses +A sealed parent interface or class can be used to directly register all its children using `subclassesOfSealed`. +This will allow serializing the children using open polymorphism without the need to register each one individually. + +If one of the type's subclasses is a sealed serializable class on its own, its subclasses are registered recursively +as well. However, if one of the type's subclasses is an open polymorphic class, an [IllegalArgumentException] is thrown. +In other words, all children/descendants must be either concrete or sealed. + + + + + +```kotlin +interface Base + +@Serializable +sealed interface Sub: Base + +@Serializable +class Sub1(val data: String): Sub + +val module1 = SerializersModule { + polymorphic(Base::class) { + subclassesOfSealed(Sub.serializer()) + } +} + +val format1 = Json { serializersModule = module1 } +``` + +Alternatively the convenience overload allows specifying the sealed type as type parameter. + +```kotlin +val module2 = SerializersModule { + polymorphic(Base::class) { + subclassesOfSealed() + } +} + +val format2 = Json { serializersModule = module2 } +``` + +Now if we declare `data` with the type of `Base` we can simply call `format.encodeToString` as before. +```kotlin + +fun main() { + val data: Base = Sub1("kotlin") + println(format1.encodeToString(data)) + println(format2.encodeToString(data)) +} +``` + +```text +{"type":"example.examplePoly11.Sub1","data":"kotlin"} +{"type":"example.examplePoly11.Sub1","data":"kotlin"} +``` + +> You can get the full code [here](../guide/example/example-poly-11.kt). + + ### Property of an interface type @@ -451,7 +514,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-poly-11.kt). +> You can get the full code [here](../guide/example/example-poly-12.kt). As long as we've registered the actual subtype of the interface that is being serialized in the [SerializersModule] of our `format`, we get it working at runtime. @@ -496,7 +559,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-poly-12.kt). +> You can get the full code [here](../guide/example/example-poly-13.kt). We get the exception. @@ -544,7 +607,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-poly-13.kt). +> You can get the full code [here](../guide/example/example-poly-14.kt). However, `Any` is a class and it is not serializable: @@ -586,7 +649,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-poly-14.kt). +> You can get the full code [here](../guide/example/example-poly-15.kt). With the explicit serializer it works as before. @@ -639,7 +702,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-poly-15.kt). +> You can get the full code [here](../guide/example/example-poly-16.kt). -> You can get the full code [here](../guide/example/example-poly-16.kt). +> You can get the full code [here](../guide/example/example-poly-17.kt).