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..a6a7ada907 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt @@ -19,15 +19,38 @@ 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]. + */ + public inline fun subclassesOf(): Unit = + subclassesOf(serializer()) + + + /** + * Registers the subclasses of the given class as subclasses of the outer class. This currently requires `baseClass` + * to be sealed. + */ + public fun subclassesOf(serializer: KSerializer) { + require(serializer is SealedClassSerializer) { + "subClassesOf only supports automatic adding of subclasses of sealed types." + } + for ((subsubclass, subserializer) in serializer.class2Serializer.entries) { + @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 +139,9 @@ 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]. + */ +public inline fun PolymorphicModuleBuilder.subclassesOf(clazz: KClass): Unit = + subclassesOf(clazz.serializer()) diff --git a/docs/polymorphism.md b/docs/polymorphism.md index 67f0560a0d..e600b7ddc5 100644 --- a/docs/polymorphism.md +++ b/docs/polymorphism.md @@ -410,6 +410,11 @@ 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 `subclassesOf`. 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. + ### 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..47a3788dca --- /dev/null +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/features/PolymorphicSealedChildTest.kt @@ -0,0 +1,65 @@ +/* + * 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.Json +import kotlinx.serialization.modules.* +import kotlinx.serialization.test.assertStringFormAndRestored +import kotlin.test.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() + } + + val sealedModule = SerializersModule { + polymorphic(FooBase::class) { + subclassesOf() + } + } + + val json = Json { serializersModule = sealedModule } + + @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 + ) + } +}