diff --git a/formats/json/jsMain/src/kotlinx/serialization/json/Dynamics.kt b/formats/json/jsMain/src/kotlinx/serialization/json/Dynamics.kt index 257dc2ac87..836f1604e1 100644 --- a/formats/json/jsMain/src/kotlinx/serialization/json/Dynamics.kt +++ b/formats/json/jsMain/src/kotlinx/serialization/json/Dynamics.kt @@ -49,7 +49,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. * 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 { 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) } } diff --git a/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt b/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt index 514853f62e..09ff6e71e8 100644 --- a/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt +++ b/formats/json/jsTest/src/kotlinx/serialization/json/DynamicPolymorphismTest.kt @@ -21,6 +21,18 @@ 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 + @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 @@ -103,6 +115,135 @@ 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 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)) + } + } + + @Test + fun testEmptyList() { + 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 testDefaultValue() { + 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)) + } + + } + + @Test + fun testNonDefaultValue() { + 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