diff --git a/runtime/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt b/runtime/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt index e166a09735..639b3b92e8 100644 --- a/runtime/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt +++ b/runtime/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt @@ -53,8 +53,8 @@ internal fun JsonInput.decodeSerializableValuePolymorphic(deserializer: Dese } val jsonTree = cast(decodeJson()) - val type = jsonTree.getValue(json.configuration.classDiscriminator).content - (jsonTree.content as MutableMap).remove(json.configuration.classDiscriminator) + val discriminator = json.configuration.classDiscriminator + val type = jsonTree.getValue(discriminator).content val actualSerializer = deserializer.findPolymorphicSerializer(this, type).cast() - return json.readJson(jsonTree, actualSerializer) + return json.readPolymorphicJson(discriminator, jsonTree, actualSerializer) } diff --git a/runtime/commonMain/src/kotlinx/serialization/json/internal/TreeJsonInput.kt b/runtime/commonMain/src/kotlinx/serialization/json/internal/TreeJsonInput.kt index 76acb039c1..6572eb01c7 100644 --- a/runtime/commonMain/src/kotlinx/serialization/json/internal/TreeJsonInput.kt +++ b/runtime/commonMain/src/kotlinx/serialization/json/internal/TreeJsonInput.kt @@ -21,6 +21,14 @@ internal fun Json.readJson(element: JsonElement, deserializer: Deserializati return input.decode(deserializer) } +internal fun Json.readPolymorphicJson( + discriminator: String, + element: JsonObject, + deserializer: DeserializationStrategy +): T { + return JsonTreeInput(this, element, discriminator, deserializer.descriptor).decode(deserializer) +} + private sealed class AbstractJsonTreeInput( override val json: Json, open val value: JsonElement @@ -140,7 +148,12 @@ private class JsonPrimitiveInput(json: Json, override val value: JsonPrimitive) } } -private open class JsonTreeInput(json: Json, override val value: JsonObject) : AbstractJsonTreeInput(json, value) { +private open class JsonTreeInput( + json: Json, + override val value: JsonObject, + private val polyDiscriminator: String? = null, + private val polyDescriptor: SerialDescriptor? = null +) : AbstractJsonTreeInput(json, value) { private var position = 0 override fun decodeElementIndex(descriptor: SerialDescriptor): Int { @@ -155,12 +168,23 @@ private open class JsonTreeInput(json: Json, override val value: JsonObject) : A override fun currentElement(tag: String): JsonElement = value.getValue(tag) + override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { + /* + * For polymorphic serialization we'd like to avoid excessive decoder creating in + * beginStructure to properly preserve 'polyDiscriminator' field and filter it out. + */ + if (descriptor === polyDescriptor) return this + return super.beginStructure(descriptor) + } + override fun endStructure(descriptor: SerialDescriptor) { if (configuration.ignoreUnknownKeys || descriptor.kind is PolymorphicKind) return // Validate keys val names = descriptor.cachedSerialNames() for (key in value.keys) { - if (key !in names) throw UnknownKeyException(key, value.toString()) + if (key !in names && key != polyDiscriminator) { + throw UnknownKeyException(key, value.toString()) + } } } } diff --git a/runtime/commonTest/src/kotlinx/serialization/features/PolymorphismTest.kt b/runtime/commonTest/src/kotlinx/serialization/features/PolymorphismTest.kt index 8e233fff11..8cb51e4d15 100644 --- a/runtime/commonTest/src/kotlinx/serialization/features/PolymorphismTest.kt +++ b/runtime/commonTest/src/kotlinx/serialization/features/PolymorphismTest.kt @@ -49,7 +49,7 @@ class PolymorphismTest : JsonTestBase() { object PolyDefaultSerializer : JsonTransformingSerializer(PolyDefault.serializer(), "foo") { override fun readTransform(element: JsonElement): JsonElement { return buildJson { - add("json", element) + add("json", JsonObject(element.jsonObject.filterKeys { it != "type" })) add("id", 42) } } diff --git a/runtime/commonTest/src/kotlinx/serialization/features/SealedPolymorphismTest.kt b/runtime/commonTest/src/kotlinx/serialization/features/SealedPolymorphismTest.kt index f264b1c781..5531070aa0 100644 --- a/runtime/commonTest/src/kotlinx/serialization/features/SealedPolymorphismTest.kt +++ b/runtime/commonTest/src/kotlinx/serialization/features/SealedPolymorphismTest.kt @@ -10,21 +10,24 @@ import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.test.assertStringFormAndRestored import kotlin.test.Test -@Serializable -data class FooHolder( - val someMetadata: Int, - val payload: List<@Polymorphic Foo> -) - -@Serializable -sealed class Foo { - @Serializable - data class Bar(val bar: Int) : Foo() +class SealedPolymorphismTest { + @Serializable - data class Baz(val baz: String) : Foo() -} + data class FooHolder( + val someMetadata: Int, + val payload: List<@Polymorphic Foo> + ) -class SealedPolymorphismTest { + @Serializable + @SerialName("Foo") + sealed class Foo { + @Serializable + @SerialName("Bar") + data class Bar(val bar: Int) : Foo() + @Serializable + @SerialName("Baz") + data class Baz(val baz: String) : Foo() + } val sealedModule = SerializersModule { polymorphic(Foo::class) { @@ -39,8 +42,8 @@ class SealedPolymorphismTest { fun testSaveSealedClassesList() { assertStringFormAndRestored( """{"someMetadata":42,"payload":[ - |{"type":"kotlinx.serialization.features.Foo.Bar","bar":1}, - |{"type":"kotlinx.serialization.features.Foo.Baz","baz":"2"}]}""".trimMargin().replace("\n", ""), + |{"type":"Bar","bar":1}, + |{"type":"Baz","baz":"2"}]}""".trimMargin().replace("\n", ""), FooHolder(42, listOf(Foo.Bar(1), Foo.Baz("2"))), FooHolder.serializer(), json, @@ -51,7 +54,7 @@ class SealedPolymorphismTest { @Test fun testCanSerializeSealedClassPolymorphicallyOnTopLevel() { assertStringFormAndRestored( - """{"type":"kotlinx.serialization.features.Foo.Bar","bar":1}""", + """{"type":"Bar","bar":1}""", Foo.Bar(1), PolymorphicSerializer(Foo::class), json diff --git a/runtime/commonTest/src/kotlinx/serialization/json/polymorphic/JsonDeserializePolymorphicTwiceTest.kt b/runtime/commonTest/src/kotlinx/serialization/json/polymorphic/JsonDeserializePolymorphicTwiceTest.kt new file mode 100644 index 0000000000..298fd91623 --- /dev/null +++ b/runtime/commonTest/src/kotlinx/serialization/json/polymorphic/JsonDeserializePolymorphicTwiceTest.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.json.polymorphic + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.* +import kotlinx.serialization.test.* +import kotlin.test.* + +class JsonDeserializePolymorphicTwiceTest { + + @Serializable + sealed class Foo { + @Serializable + data class Bar(val a: Int) : Foo() + } + + @Test + fun testDeserializeTwice() { // #812 + val json = Json.toJson(Foo.serializer(), Foo.Bar(1)) + assertEquals(Foo.Bar(1), Json.fromJson(Foo.serializer(), json)) + assertEquals(Foo.Bar(1), Json.fromJson(Foo.serializer(), json)) + } +} \ No newline at end of file