From 8c45f6cd67f9960bf93fcd52769a38e4e6419fa6 Mon Sep 17 00:00:00 2001 From: Ankush Gupta Date: Thu, 12 Nov 2020 17:57:54 -0500 Subject: [PATCH 1/9] add test to reproduce issue --- .../json/DynamicPolymorphismTest.kt | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt b/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt index 514853f62e..c03c7f7e35 100644 --- a/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt +++ b/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt @@ -21,6 +21,10 @@ class DynamicPolymorphismTest { @Serializable @SerialName("type_child") data class TypeChild(val type: String) : Sealed(2) + + @Serializable + @SerialName("nullable_child") + data class NullableChild(val nullable: String?): Sealed(3) } @Serializable @@ -103,6 +107,38 @@ class DynamicPolymorphismTest { } } + @Test + fun testNullable() { + val nonNullChild = Sealed.NullableChild("nonnull") + encodeAndDecode(Sealed.serializer(), nonNullChild, arrayJson) { + assertEquals("nullable_child", this[0]) + val dynamicValue = this[1] + assertEquals(nonNullChild.nullable, dynamicValue.nullable) + assertEquals(nonNullChild.intField, dynamicValue.intField) + assertEquals(2, fieldsCount(dynamicValue)) + } + encodeAndDecode(Sealed.serializer(), nonNullChild, objectJson) { + assertEquals("nullable_child", this.type) + assertEquals(nonNullChild.nullable, this.nullable) + assertEquals(nonNullChild.intField, this.intField) + assertEquals(3, fieldsCount(this)) + } + + val nullChild = Sealed.NullableChild(null) + encodeAndDecode(Sealed.serializer(), nullChild, arrayJson) { + assertEquals("nullable_child", this[0]) + val dynamicValue = this[1] + assertEquals(nullChild.nullable, dynamicValue.nullable) + assertEquals(nullChild.intField, dynamicValue.intField) + assertEquals(2, fieldsCount(dynamicValue)) + } + encodeAndDecode(Sealed.serializer(), nullChild, objectJson) { + assertEquals("nullable_child", this.type) + assertEquals(nullChild.nullable, this.nullable) + assertEquals(nullChild.intField, this.intField) + assertEquals(3, fieldsCount(this)) + } + } @Test fun testObject() { val value = Sealed.ObjectChild From ecad9894a74f87c658e55a6a676e708b5d6de6ea Mon Sep 17 00:00:00 2001 From: Ankush Gupta Date: Thu, 12 Nov 2020 17:58:13 -0500 Subject: [PATCH 2/9] fix the problem --- .../kotlinx/serialization/json/internal/DynamicDecoders.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/formats/json/jsMain/src/kotlinx/serialization/json/internal/DynamicDecoders.kt b/formats/json/jsMain/src/kotlinx/serialization/json/internal/DynamicDecoders.kt index 4fa70789a9..c47a140f1e 100644 --- a/formats/json/jsMain/src/kotlinx/serialization/json/internal/DynamicDecoders.kt +++ b/formats/json/jsMain/src/kotlinx/serialization/json/internal/DynamicDecoders.kt @@ -49,6 +49,10 @@ private open class DynamicInput( return json.decodeFromDynamic(JsonElement.serializer(), value[tag]) } + if (value == null) { + return JsonNull + } + val keys: dynamic = js("Object").keys(value) val size: Int = keys.length as Int return buildJsonObject { From 9e8561ffa49057a5174edf0a550a7baad0fae876 Mon Sep 17 00:00:00 2001 From: Ankush Gupta Date: Thu, 12 Nov 2020 18:01:06 -0500 Subject: [PATCH 3/9] remove docs saying polymorphism doesn't work for encode/decodeFromDynamic --- formats/json/jsMain/src/kotlinx/serialization/json/Dynamics.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/formats/json/jsMain/src/kotlinx/serialization/json/Dynamics.kt b/formats/json/jsMain/src/kotlinx/serialization/json/Dynamics.kt index 257dc2ac87..a683dd0fad 100644 --- a/formats/json/jsMain/src/kotlinx/serialization/json/Dynamics.kt +++ b/formats/json/jsMain/src/kotlinx/serialization/json/Dynamics.kt @@ -13,7 +13,6 @@ import kotlinx.serialization.json.internal.* * * A result of `decodeFromDynamic(nativeObj)` should be the same as * `kotlinx.serialization.json.Json.decodeFromString(kotlin.js.JSON.stringify(nativeObj))`. - * This class also supports array-based polymorphism if the corresponding flag in [Json.configuration] is set to `true`. * Does not support any other [Map] keys than [String]. * Has limitation on [Long] type: any JS number that is greater than * [`abs(2^53-1)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER) @@ -49,7 +48,6 @@ public inline fun Json.decodeFromDynamic(dynamic: dynamic): T = * * Limitations: * * Map keys must be of primitive or enum type - * * Currently does not support polymorphism * * All [Long] values must be less than [`abs(2^53-1)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER). * Otherwise, they're encoded as doubles with precision loss and require `isLenient` flag of [Json.configuration] set to true. * From fe6928b6ac8ccf06ffdaf6a1ae7535dab9f78b23 Mon Sep 17 00:00:00 2001 From: Ankush Gupta Date: Thu, 12 Nov 2020 18:04:34 -0500 Subject: [PATCH 4/9] add tests to confirm that lists (incl emptyList) work with polymorphic dynamic serialization --- .../json/DynamicPolymorphismTest.kt | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt b/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt index c03c7f7e35..8a909cf86a 100644 --- a/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt +++ b/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt @@ -25,6 +25,11 @@ class DynamicPolymorphismTest { @Serializable @SerialName("nullable_child") data class NullableChild(val nullable: String?): Sealed(3) + + @Serializable + @SerialName("list_child") + data class ListChild(val list: List): Sealed(4) + } @Serializable @@ -139,6 +144,40 @@ class DynamicPolymorphismTest { assertEquals(3, fieldsCount(this)) } } + + @Test + fun testList() { + val listChild = Sealed.ListChild(listOf("one", "two")) + encodeAndDecode(Sealed.serializer(), listChild, arrayJson) { + assertEquals("list_child", this[0]) + val dynamicValue = this[1] + assertEquals(listChild.list, (dynamicValue.list as Array).toList()) + assertEquals(listChild.intField, dynamicValue.intField) + assertEquals(2, fieldsCount(dynamicValue)) + } + encodeAndDecode(Sealed.serializer(), listChild, objectJson) { + assertEquals("list_child", this.type) + assertEquals(listChild.list, (this.list as Array).toList()) + assertEquals(listChild.intField, this.intField) + assertEquals(3, fieldsCount(this)) + } + + + val emptyListChild = Sealed.ListChild(emptyList()) + encodeAndDecode(Sealed.serializer(), emptyListChild, arrayJson) { + assertEquals("list_child", this[0]) + val dynamicValue = this[1] + assertEquals(emptyListChild.list, (dynamicValue.list as Array).toList()) + assertEquals(emptyListChild.intField, dynamicValue.intField) + assertEquals(2, fieldsCount(dynamicValue)) + } + encodeAndDecode(Sealed.serializer(), emptyListChild, objectJson) { + assertEquals("list_child", this.type) + assertEquals(emptyListChild.list, (this.list as Array).toList()) + assertEquals(emptyListChild.intField, this.intField) + assertEquals(3, fieldsCount(this)) + } + } @Test fun testObject() { val value = Sealed.ObjectChild From 0b18a44663b384f41a8ba308c5715220d9c0af34 Mon Sep 17 00:00:00 2001 From: Ankush Gupta Date: Thu, 12 Nov 2020 18:05:30 -0500 Subject: [PATCH 5/9] add tests to confirm that nullable default params work with polymorphic dynamic serialization, and support encodeDefaults --- .../json/DynamicPolymorphismTest.kt | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt b/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt index 8a909cf86a..74f5353675 100644 --- a/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt +++ b/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt @@ -30,6 +30,9 @@ class DynamicPolymorphismTest { @SerialName("list_child") data class ListChild(val list: List): Sealed(4) + @Serializable + @SerialName("default_child") + data class DefaultChild(val default: String? = "default"): Sealed(5) } @Serializable @@ -178,6 +181,63 @@ class DynamicPolymorphismTest { assertEquals(3, fieldsCount(this)) } } + + @Test + fun testDefault() { + val objectJsonWithDefaults = Json(objectJson) { + encodeDefaults = true + } + + val arrayJsonWithDefaults = Json(arrayJson) { + encodeDefaults = true + } + + val defaultChild = Sealed.DefaultChild() + encodeAndDecode(Sealed.serializer(), defaultChild, arrayJson) { + assertEquals("default_child", this[0]) + val dynamicValue = this[1] + assertEquals(null, dynamicValue.default, "arrayJson should not encode defaults") + assertEquals(defaultChild.intField, dynamicValue.intField) + assertEquals(1, fieldsCount(dynamicValue)) + } + encodeAndDecode(Sealed.serializer(), defaultChild, arrayJsonWithDefaults) { + assertEquals("default_child", this[0]) + val dynamicValue = this[1] + assertEquals(defaultChild.default, dynamicValue.default, "arrayJsonWithDefaults should encode defaults") + assertEquals(defaultChild.intField, dynamicValue.intField) + assertEquals(2, fieldsCount(dynamicValue)) + } + + encodeAndDecode(Sealed.serializer(), defaultChild, objectJson) { + assertEquals("default_child", this.type) + assertEquals(null, this.default, "objectJson should not encode defaults") + assertEquals(defaultChild.intField, this.intField) + assertEquals(2, fieldsCount(this)) + } + encodeAndDecode(Sealed.serializer(), defaultChild, objectJsonWithDefaults) { + assertEquals("default_child", this.type) + assertEquals(defaultChild.default, this.default, "objectJsonWithDefaults should encode defaults") + assertEquals(defaultChild.intField, this.intField) + assertEquals(3, fieldsCount(this)) + } + + val nonDefaultChild = Sealed.DefaultChild("non default value") + encodeAndDecode(Sealed.serializer(), nonDefaultChild, arrayJson) { + assertEquals("default_child", this[0]) + val dynamicValue = this[1] + assertEquals(nonDefaultChild.default, dynamicValue.default) + assertEquals(nonDefaultChild.intField, dynamicValue.intField) + assertEquals(2, fieldsCount(dynamicValue)) + } + + encodeAndDecode(Sealed.serializer(), nonDefaultChild, objectJson) { + assertEquals("default_child", this.type) + assertEquals(nonDefaultChild.default, this.default) + assertEquals(nonDefaultChild.intField, this.intField) + assertEquals(3, fieldsCount(this)) + } + } + @Test fun testObject() { val value = Sealed.ObjectChild From daa7629f8672e055a1421b76c6c9190213bede1b Mon Sep 17 00:00:00 2001 From: Ankush Gupta Date: Thu, 19 Nov 2020 12:21:38 -0500 Subject: [PATCH 6/9] update docs --- formats/json/jsMain/src/kotlinx/serialization/json/Dynamics.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/formats/json/jsMain/src/kotlinx/serialization/json/Dynamics.kt b/formats/json/jsMain/src/kotlinx/serialization/json/Dynamics.kt index a683dd0fad..18ce9fca74 100644 --- a/formats/json/jsMain/src/kotlinx/serialization/json/Dynamics.kt +++ b/formats/json/jsMain/src/kotlinx/serialization/json/Dynamics.kt @@ -13,6 +13,7 @@ import kotlinx.serialization.json.internal.* * * A result of `decodeFromDynamic(nativeObj)` should be the same as * `kotlinx.serialization.json.Json.decodeFromString(kotlin.js.JSON.stringify(nativeObj))`. + * This class also supports array-based polymorphism if the corresponding flag in [Json.configuration] is set to `true`. * Does not support any other [Map] keys than [String]. * Has limitation on [Long] type: any JS number that is greater than * [`abs(2^53-1)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER) @@ -44,7 +45,7 @@ public inline fun Json.decodeFromDynamic(dynamic: dynamic): T = decodeFromDynamic(serializersModule.serializer(), dynamic) /** - * Converts Kotlin data structures to plain Javascript objects + * Converts Kotlin data structures to plain Javascript objects, including support for polymorphism * * Limitations: * * Map keys must be of primitive or enum type From 664b00ad3230c365cc2f4cbbba1d723c5d42a05f Mon Sep 17 00:00:00 2001 From: Ankush Gupta Date: Thu, 19 Nov 2020 12:31:35 -0500 Subject: [PATCH 7/9] add tests in DecodeFromDynamicSpecialCasesTest --- .../json/DecodeFromDynamicSpecialCasesTest.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/formats/json/jsTest/src/kotlinx/serialization/json/DecodeFromDynamicSpecialCasesTest.kt b/formats/json/jsTest/src/kotlinx/serialization/json/DecodeFromDynamicSpecialCasesTest.kt index 3536ac1863..748a2dced7 100644 --- a/formats/json/jsTest/src/kotlinx/serialization/json/DecodeFromDynamicSpecialCasesTest.kt +++ b/formats/json/jsTest/src/kotlinx/serialization/json/DecodeFromDynamicSpecialCasesTest.kt @@ -79,6 +79,12 @@ class DecodeFromDynamicSpecialCasesTest { testJsonElement(js(""""42""""), JsonPrimitive("42")) } + @Test + fun testJsonNull() { + testJsonElement(js("null"), JsonNull) + testJsonElement(js("undefined"), JsonNull) + } + @Test fun testJsonArray() { testJsonElement(js("[1,2,3]"), JsonArray((1..3).map(::JsonPrimitive))) @@ -103,13 +109,13 @@ class DecodeFromDynamicSpecialCasesTest { } @Serializable - data class Wrapper(val e: JsonElement, val p: JsonPrimitive, val o: JsonObject, val a: JsonArray) + data class Wrapper(val e: JsonElement, val p: JsonPrimitive, val o: JsonObject, val a: JsonArray, val n: JsonNull) @Test fun testJsonElementWrapper() { - val js = js("""{"e":42,"p":"239", "o":{"k":"v"}, "a":[1, 2, 3]}""") + val js = js("""{"e":42,"p":"239", "o":{"k":"v"}, "a":[1, 2, 3], "n": null}""") val parsed = Json.decodeFromDynamic(js) - val expected = Wrapper(JsonPrimitive(42), JsonPrimitive("239"), buildJsonObject { put("k", "v") }, JsonArray((1..3).map(::JsonPrimitive))) + val expected = Wrapper(JsonPrimitive(42), JsonPrimitive("239"), buildJsonObject { put("k", "v") }, JsonArray((1..3).map(::JsonPrimitive)), JsonNull) assertEquals(expected, parsed) } } From 6f3c61a559eee00185cc8038581c26d6bb58c610 Mon Sep 17 00:00:00 2001 From: Ankush Gupta Date: Thu, 19 Nov 2020 12:37:19 -0500 Subject: [PATCH 8/9] split test cases in DynamicPolymorphismTest --- .../serialization/json/DynamicPolymorphismTest.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt b/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt index 74f5353675..09ff6e71e8 100644 --- a/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt +++ b/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt @@ -164,8 +164,10 @@ class DynamicPolymorphismTest { assertEquals(listChild.intField, this.intField) assertEquals(3, fieldsCount(this)) } + } - + @Test + fun testEmptyList() { val emptyListChild = Sealed.ListChild(emptyList()) encodeAndDecode(Sealed.serializer(), emptyListChild, arrayJson) { assertEquals("list_child", this[0]) @@ -183,7 +185,7 @@ class DynamicPolymorphismTest { } @Test - fun testDefault() { + fun testDefaultValue() { val objectJsonWithDefaults = Json(objectJson) { encodeDefaults = true } @@ -221,6 +223,10 @@ class DynamicPolymorphismTest { assertEquals(3, fieldsCount(this)) } + } + + @Test + fun testNonDefaultValue() { val nonDefaultChild = Sealed.DefaultChild("non default value") encodeAndDecode(Sealed.serializer(), nonDefaultChild, arrayJson) { assertEquals("default_child", this[0]) From c3d2e31b8670f87c53ea8ae26c8c1c2240cb674f Mon Sep 17 00:00:00 2001 From: Ankush Gupta Date: Fri, 20 Nov 2020 09:43:27 -0500 Subject: [PATCH 9/9] Update formats/json/jsMain/src/kotlinx/serialization/json/Dynamics.kt Co-authored-by: Leonid Startsev --- formats/json/jsMain/src/kotlinx/serialization/json/Dynamics.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/formats/json/jsMain/src/kotlinx/serialization/json/Dynamics.kt b/formats/json/jsMain/src/kotlinx/serialization/json/Dynamics.kt index 18ce9fca74..836f1604e1 100644 --- a/formats/json/jsMain/src/kotlinx/serialization/json/Dynamics.kt +++ b/formats/json/jsMain/src/kotlinx/serialization/json/Dynamics.kt @@ -45,7 +45,7 @@ public inline fun Json.decodeFromDynamic(dynamic: dynamic): T = decodeFromDynamic(serializersModule.serializer(), dynamic) /** - * Converts Kotlin data structures to plain Javascript objects, including support for polymorphism + * Converts Kotlin data structures to plain Javascript objects * * Limitations: * * Map keys must be of primitive or enum type