From 114f68b130558ee4eb3ad69ecc705eaf43a07f08 Mon Sep 17 00:00:00 2001 From: Jan Seeger Date: Thu, 14 Mar 2024 15:37:26 +0100 Subject: [PATCH] Add generic sealed class support Currently only String type properties are supported. Enums will follow. --- .../common/SealedClassWithTypeSerializer.kt | 57 +++++++++++++++++++ .../common/StringMapToObjectDecoder.kt | 11 +++- .../federmappe/common/TypeAwareDecoder.kt | 5 ++ .../federmappe/common/SealedTypeTests.kt | 54 ++++++++++++------ .../common/SubclassDecodingTests.kt | 2 +- 5 files changed, 108 insertions(+), 21 deletions(-) create mode 100644 common/src/main/kotlin/de/sipgate/federmappe/common/SealedClassWithTypeSerializer.kt create mode 100644 common/src/main/kotlin/de/sipgate/federmappe/common/TypeAwareDecoder.kt diff --git a/common/src/main/kotlin/de/sipgate/federmappe/common/SealedClassWithTypeSerializer.kt b/common/src/main/kotlin/de/sipgate/federmappe/common/SealedClassWithTypeSerializer.kt new file mode 100644 index 0000000..5fea69b --- /dev/null +++ b/common/src/main/kotlin/de/sipgate/federmappe/common/SealedClassWithTypeSerializer.kt @@ -0,0 +1,57 @@ +package de.sipgate.federmappe.common + +import kotlin.reflect.KClass +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.PolymorphicKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.serializerOrNull + +@ExperimentalSerializationApi +@InternalSerializationApi +abstract class SealedClassWithTypeSerializer( + private val baseClass: KClass, + private val discriminatorName: String = "type" +) : KSerializer { + + override val descriptor: SerialDescriptor = + buildSerialDescriptor( + "JsonContentPolymorphicSerializer<${baseClass.simpleName}>", + PolymorphicKind.SEALED + ) + + final override fun serialize(encoder: Encoder, value: Type) { + val actualSerializer = + encoder.serializersModule.getPolymorphic(baseClass, value) + ?: value::class.serializerOrNull() + ?: throwSubtypeNotRegistered(value::class, baseClass) + @Suppress("UNCHECKED_CAST") + (actualSerializer as KSerializer).serialize(encoder, value) + } + + final override fun deserialize(decoder: Decoder): Type { + val type = (decoder as? TypeAwareDecoder) + ?.decodeType(typeKey = discriminatorName) + ?: throw IllegalStateException("We need to know the type to decode first!") + + val actualSerializer = selectDeserializer(type) as KSerializer + return decoder.decodeSerializableValue(actualSerializer) + } + + protected abstract fun selectDeserializer(element: DiscriminatorType): DeserializationStrategy + + private fun throwSubtypeNotRegistered(subClass: KClass<*>, baseClass: KClass<*>): Nothing { + val subClassName = subClass.simpleName ?: "$subClass" + val scope = "in the scope of '${baseClass.simpleName}'" + throw SerializationException( + "Class '${subClassName}' is not registered for polymorphic serialization $scope.\n" + + "Mark the base class as 'sealed' or register the serializer explicitly." + ) + } +} diff --git a/common/src/main/kotlin/de/sipgate/federmappe/common/StringMapToObjectDecoder.kt b/common/src/main/kotlin/de/sipgate/federmappe/common/StringMapToObjectDecoder.kt index 17d8e02..df3c374 100644 --- a/common/src/main/kotlin/de/sipgate/federmappe/common/StringMapToObjectDecoder.kt +++ b/common/src/main/kotlin/de/sipgate/federmappe/common/StringMapToObjectDecoder.kt @@ -2,6 +2,7 @@ package de.sipgate.federmappe.common import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.PolymorphicKind import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.StructureKind import kotlinx.serialization.encoding.AbstractDecoder @@ -15,7 +16,7 @@ class StringMapToObjectDecoder( override val serializersModule: SerializersModule = EmptySerializersModule(), private val ignoreUnknownProperties: Boolean = false, private val subtypeDecoder: (Any?) -> CompositeDecoder? = { null } -) : AbstractDecoder() { +) : AbstractDecoder(), TypeAwareDecoder { private val keysIterator = data.keys.iterator() private var index: Int? = null private var key: String? = null @@ -64,7 +65,7 @@ class StringMapToObjectDecoder( } when (valueDescriptor) { - StructureKind.CLASS -> return StringMapToObjectDecoder( + StructureKind.CLASS, PolymorphicKind.SEALED -> return StringMapToObjectDecoder( data = value as Map, ignoreUnknownProperties = ignoreUnknownProperties, serializersModule = this.serializersModule, @@ -98,4 +99,10 @@ class StringMapToObjectDecoder( super.endStructure(descriptor) } + + override fun decodeType(typeKey: String): T? { + @Suppress("UNCHECKED_CAST") + val currentData = (data[key] as? Map) ?: return null + return (currentData[typeKey] as? T) + } } diff --git a/common/src/main/kotlin/de/sipgate/federmappe/common/TypeAwareDecoder.kt b/common/src/main/kotlin/de/sipgate/federmappe/common/TypeAwareDecoder.kt new file mode 100644 index 0000000..6018093 --- /dev/null +++ b/common/src/main/kotlin/de/sipgate/federmappe/common/TypeAwareDecoder.kt @@ -0,0 +1,5 @@ +package de.sipgate.federmappe.common + +interface TypeAwareDecoder { + fun decodeType(typeKey: String = "type"): T? +} diff --git a/common/src/test/kotlin/de/sipgate/federmappe/common/SealedTypeTests.kt b/common/src/test/kotlin/de/sipgate/federmappe/common/SealedTypeTests.kt index c0b3149..a4a5443 100644 --- a/common/src/test/kotlin/de/sipgate/federmappe/common/SealedTypeTests.kt +++ b/common/src/test/kotlin/de/sipgate/federmappe/common/SealedTypeTests.kt @@ -1,34 +1,44 @@ package de.sipgate.federmappe.common +import de.sipgate.federmappe.common.SealedTypeTests.BaseType +import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Polymorphic -import kotlinx.serialization.SerialName +import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.Serializable +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic +import kotlinx.serialization.modules.subclass import kotlinx.serialization.serializer import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertInstanceOf import org.junit.jupiter.api.Test -@OptIn(ExperimentalSerializationApi::class) -class SealedTypeTests { - - enum class TypeDecl { - @SerialName("A_TYPE") A, - @SerialName("B_TYPE") B +@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) +internal class CustomSerializer : SealedClassWithTypeSerializer(BaseType::class) { + override fun selectDeserializer(element: String): DeserializationStrategy { + return when (element) { + "A" -> BaseType.A.serializer() + "B" -> BaseType.B.serializer() + else -> throw IllegalArgumentException("unknown element $element") + } } +} - @Serializable +@OptIn(ExperimentalSerializationApi::class) +internal class SealedTypeTests { + + @Serializable(with = CustomSerializer::class) sealed interface BaseType { - val type: TypeDecl + val type: String @Serializable - data class A(val value: String): BaseType { - override val type = TypeDecl.A + data class A(val value: String) : BaseType { + override val type = "A" } @Serializable - data class B(val value: Boolean): BaseType { - override val type = TypeDecl.B + data class B(val value: Boolean) : BaseType { + override val type = "B" } } @@ -39,10 +49,12 @@ class SealedTypeTests { data class TestClass(val a: BaseType) val serializer = serializer() - val data = mapOf("a" to mapOf( - "type" to "A_TYPE", - "value" to "some string" - )) + val data = mapOf( + "a" to mapOf( + "type" to "A", + "value" to "some string" + ) + ) // Act val result = @@ -50,6 +62,12 @@ class SealedTypeTests { StringMapToObjectDecoder( data, ignoreUnknownProperties = true, + serializersModule = SerializersModule { + polymorphic(BaseType::class) { + subclass(BaseType.A::class) + subclass(BaseType.B::class) + } + } ), ) diff --git a/common/src/test/kotlin/de/sipgate/federmappe/common/SubclassDecodingTests.kt b/common/src/test/kotlin/de/sipgate/federmappe/common/SubclassDecodingTests.kt index 4fefb14..403c029 100644 --- a/common/src/test/kotlin/de/sipgate/federmappe/common/SubclassDecodingTests.kt +++ b/common/src/test/kotlin/de/sipgate/federmappe/common/SubclassDecodingTests.kt @@ -11,7 +11,7 @@ import org.junit.jupiter.api.Test @OptIn(ExperimentalSerializationApi::class) class SubclassDecodingTests { @Serializable - data class A(val b: String) + internal data class A(val b: String) @Test fun deserializeNestedDataClass() {