diff --git a/core/api/kotlinx-serialization-core.api b/core/api/kotlinx-serialization-core.api index 1b15d35953..4c16ae3664 100644 --- a/core/api/kotlinx-serialization-core.api +++ b/core/api/kotlinx-serialization-core.api @@ -1334,6 +1334,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 5aefad0f2f..14eda2a56b 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] @@ -1168,6 +1170,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(kotlin.reflect/KClass<#B>) // kotlinx.serialization.modules/subclassesOfSealed|subclassesOfSealed@kotlinx.serialization.modules.PolymorphicModuleBuilder<0:0>(kotlin.reflect.KClass<0:1>){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..a76a922c3a 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.* @@ -19,15 +20,87 @@ public class PolymorphicModuleBuilder @PublishedApi internal cons private val baseClass: KClass, private val baseSerializer: KSerializer? = null ) { - private val subclasses: MutableList, KSerializer>> = mutableListOf() + private val subclasses: MutableMap, KSerializer> = mutableMapOf() private var defaultSerializerProvider: ((Base) -> SerializationStrategy?)? = null private var defaultDeserializerProvider: ((String?) -> DeserializationStrategy?)? = null + + /** + * Registers the child serializers for the sealed [subclass] [serializer] in the resulting module under + * the [base class][Base]. Please note that type `T` must be sealed, if not a runtime error will + * be thrown at registration time. This function is a convenience function for the version that + * receives a serializer. + * + * + * Example: + * ```kotlin + * interface Base + * + * @Serializable + * sealed interface Sub + * + * @Serializable + * class Sub1: Sub + * + * serializersModule { + * polymorphic(Base::class) { + * subclassesOfSealed() + * } + * } + * ``` + */ + @ExperimentalSerializationApi + public inline fun subclassesOfSealed(): Unit = + subclassesOfSealed(serializer()) + + + /** + * Registers the subclasses of the given class as subclasses of the outer class. This currently + * requires `serializer` to be sealed. If not a runtime error will be thrown at registration time. + * + * Example: + * ```kotlin + * interface Base + * + * @Serializable + * sealed interface Sub + * + * @Serializable + * class Sub1: Sub + * + * serializersModule { + * polymorphic(Base::class) { + * subclassesOfSealed(Sub.serializer()) + * } + * } + * ``` + * + * It is an error if th + */ + @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) { + "subClassesOf only supports automatic adding of subclasses of sealed types." + } + 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 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]. */ public fun subclass(subclass: KClass, serializer: KSerializer) { - subclasses.add(subclass to serializer) + subclasses[subclass] = serializer } /** @@ -116,3 +189,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(clazz: KClass): Unit = + subclassesOfSealed(clazz.serializer()) diff --git a/docs/polymorphism.md b/docs/polymorphism.md index 9294b67c28..857f3e8f64 100644 --- a/docs/polymorphism.md +++ b/docs/polymorphism.md @@ -414,6 +414,13 @@ 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 expose all children that would be available when serializing the parent directly, but now as sealed. Please +note that this is will remain open serialization, and the sealed parent serializer will not be used in serialization. +In addition, it is not valid if the hierarchy contains open (not sealed) polymorphic children (this will result in +an error at runtime). + ### Property of an interface type diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/features/PolymorphicSealedChildTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/features/PolymorphicSealedChildTest.kt new file mode 100644 index 0000000000..6ccbb550ee --- /dev/null +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/features/PolymorphicSealedChildTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2017-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.features + +import kotlinx.serialization.* +import kotlinx.serialization.json.* +import kotlinx.serialization.modules.* +import kotlinx.serialization.test.* +import kotlin.test.* + +class PolymorphicSealedChildTest { + + @Serializable + data class FooHolder( + val someMetadata: Int, + val payload: List<@Polymorphic FooBase> + ) + + @Serializable + abstract class FooBase + + @Serializable + @SerialName("Foo") + sealed class Foo: FooBase() { + @Serializable + @SerialName("Bar") + data class Bar(val bar: Int) : Foo() + @Serializable + @SerialName("Baz") + data class Baz(val baz: String) : Foo() + } + + @Serializable + sealed class FooOpen: FooBase() { + @Serializable + data class Bar(val bar: Int) : FooOpen() + @Serializable + abstract class Baz: FooOpen() + @Serializable + data class BazChild1(val baz: String) : Baz() + @Serializable + data class BazChild2(val baz: String) : Baz() + } + + val sealedModule = SerializersModule { + polymorphic(FooBase::class) { + subclassesOfSealed() + } + } + + val json = Json { serializersModule = sealedModule } + + @Test + fun testOpenGrandchildIsInvalid() { + assertFailsWith { + SerializersModule { + polymorphic(FooBase::class) { + subclassesOfSealed() + } + } + } + } + + @Test + fun testSaveSealedClassesList() { + assertStringFormAndRestored( + """{"someMetadata":42,"payload":[ + |{"type":"Bar","bar":1}, + |{"type":"Baz","baz":"2"}]}""".trimMargin().replace("\n", ""), + FooHolder(42, listOf(Foo.Bar(1), Foo.Baz("2"))), + FooHolder.serializer(), + json, + printResult = true + ) + } + + @Test + fun testCanSerializeSealedClassPolymorphicallyOnTopLevel() { + assertStringFormAndRestored( + """{"type":"Bar","bar":1}""", + Foo.Bar(1), + PolymorphicSerializer(FooBase::class), + json + ) + } +}