Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix dynamic serialization for nullable values #1199

Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ public inline fun <reified T> 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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ class DecodeFromDynamicSpecialCasesTest {
testJsonElement<JsonPrimitive>(js(""""42""""), JsonPrimitive("42"))
}

@Test
fun testJsonNull() {
testJsonElement<JsonElement>(js("null"), JsonNull)
testJsonElement<JsonElement>(js("undefined"), JsonNull)
}

@Test
fun testJsonArray() {
testJsonElement<JsonElement>(js("[1,2,3]"), JsonArray((1..3).map(::JsonPrimitive)))
Expand All @@ -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<Wrapper>(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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ class DynamicPolymorphismTest {
@Serializable
@SerialName("type_child")
data class TypeChild(val type: String) : Sealed(2)

ankushg marked this conversation as resolved.
Show resolved Hide resolved
@Serializable
@SerialName("nullable_child")
data class NullableChild(val nullable: String?): Sealed(3)

@Serializable
@SerialName("list_child")
data class ListChild(val list: List<String>): Sealed(4)

@Serializable
@SerialName("default_child")
data class DefaultChild(val default: String? = "default"): Sealed(5)
}

@Serializable
Expand Down Expand Up @@ -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))
}
Comment on lines +143 to +148
Copy link
Contributor Author

@ankushg ankushg Nov 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This specific test is what was failing before.

It works fine for non-null values and/or array-based serialization, but was throwing a TypeError: Cannot convert undefined or null to object when it tried js("Object").keys(null) with object-based serialization

}

@Test
fun testList() {
ankushg marked this conversation as resolved.
Show resolved Hide resolved
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<String>).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<String>).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<String>).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<String>).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
Expand Down