From 20d410e85adce0f7833d9c90db3148aeaa5c6575 Mon Sep 17 00:00:00 2001 From: Paul de Vrieze Date: Mon, 20 Feb 2023 18:30:04 +0000 Subject: [PATCH 1/2] Create an implementation that allows automatically registering the sealed children of a class for polymorphic serialization of a shared (non-sealed) type. --- .../kotlinx/serialization/SealedSerializer.kt | 2 +- .../modules/PolymorphicModuleBuilder.kt | 27 ++++++++ docs/polymorphism.md | 5 ++ .../features/PolymorphicSealedChildTest.kt | 65 +++++++++++++++++++ 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 formats/json-tests/commonTest/src/kotlinx/serialization/features/PolymorphicSealedChildTest.kt diff --git a/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt b/core/commonMain/src/kotlinx/serialization/SealedSerializer.kt index 52b7c0544..74c17f648 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 1b8d431e1..5a83e9233 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt @@ -23,6 +23,21 @@ public class PolymorphicModuleBuilder @PublishedApi internal cons private var defaultSerializerProvider: ((Base) -> SerializationStrategy?)? = null private var defaultDeserializerProvider: ((String?) -> DeserializationStrategy?)? = null + /** + * Registers the subclasses of the given class as subclasses of the outer class. Currently this requires `baseClass` + * to be sealed. + */ + public fun subclassesOf(baseClass: KClass, 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]. */ @@ -116,3 +131,15 @@ public inline fun PolymorphicModuleBuilder. */ public inline fun PolymorphicModuleBuilder.subclass(clazz: KClass): Unit = subclass(clazz, serializer()) + +/** + * Registers the child serializers for the sealed [subclass] [serializer] in the resulting module under the [base class][Base]. + */ +public inline fun PolymorphicModuleBuilder.subclassesOf(serializer: KSerializer): Unit = + subclassesOf(T::class, 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 67f0560a0..d65cc5219 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 000000000..1957026c2 --- /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(Foo.serializer()) + } + } + + 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 + ) + } +} From 4bdff601c635bc7587274d656472e94ec6d91007 Mon Sep 17 00:00:00 2001 From: Paul de Vrieze Date: Mon, 20 Feb 2023 18:58:32 +0000 Subject: [PATCH 2/2] Tidy up the code a bit (and use the cast method on the serializer to avoid casting warnings). Use a map to allow faster lookup of serializers to class (to allow for overlapping hierarchies). --- .../modules/PolymorphicModuleBuilder.kt | 24 ++++++++++--------- docs/polymorphism.md | 2 +- .../features/PolymorphicSealedChildTest.kt | 2 +- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt b/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt index 5a83e9233..a6a7ada90 100644 --- a/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt +++ b/core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt @@ -19,15 +19,23 @@ 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. Currently this requires `baseClass` + * Registers the subclasses of the given class as subclasses of the outer class. This currently requires `baseClass` * to be sealed. */ - public fun subclassesOf(baseClass: KClass, serializer: KSerializer) { + public fun subclassesOf(serializer: KSerializer) { require(serializer is SealedClassSerializer) { "subClassesOf only supports automatic adding of subclasses of sealed types." } @@ -42,7 +50,7 @@ public class PolymorphicModuleBuilder @PublishedApi internal cons * 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 } /** @@ -132,14 +140,8 @@ public inline fun PolymorphicModuleBuilder. public inline fun PolymorphicModuleBuilder.subclass(clazz: KClass): Unit = subclass(clazz, serializer()) -/** - * Registers the child serializers for the sealed [subclass] [serializer] in the resulting module under the [base class][Base]. - */ -public inline fun PolymorphicModuleBuilder.subclassesOf(serializer: KSerializer): Unit = - subclassesOf(T::class, 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()) + subclassesOf(clazz.serializer()) diff --git a/docs/polymorphism.md b/docs/polymorphism.md index d65cc5219..e600b7ddc 100644 --- a/docs/polymorphism.md +++ b/docs/polymorphism.md @@ -411,7 +411,7 @@ 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 +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. diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/features/PolymorphicSealedChildTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/features/PolymorphicSealedChildTest.kt index 1957026c2..47a3788dc 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/features/PolymorphicSealedChildTest.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/features/PolymorphicSealedChildTest.kt @@ -34,7 +34,7 @@ class PolymorphicSealedChildTest { val sealedModule = SerializersModule { polymorphic(FooBase::class) { - subclassesOf(Foo.serializer()) + subclassesOf() } }