Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ private open class DynamicInput(
}
}

override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T {
return decodeSerializableValuePolymorphic(deserializer)
}

override fun composeName(parentName: String, childName: String): String = childName

override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ private class DynamicObjectEncoder(
private lateinit var currentDescriptor: SerialDescriptor
private var currentElementIsMapKey = false

/**
* Flag of usage polymorphism with discriminator attribute
*/
private var writePolymorphic = false

private object NoOutputMark

class Node(val writeMode: WriteMode, val jsObject: dynamic) {
Expand All @@ -73,11 +78,12 @@ private class DynamicObjectEncoder(
current.index = index
currentDescriptor = descriptor

if (current.writeMode == WriteMode.MAP) {
currentElementIsMapKey = current.index % 2 == 0
} else {
currentName = descriptor.getElementName(index)
when {
current.writeMode == WriteMode.MAP -> currentElementIsMapKey = current.index % 2 == 0
current.writeMode == WriteMode.LIST && descriptor.kind is PolymorphicKind -> currentName = index.toString()
else -> currentName = descriptor.getElementName(index)
}

return true
}

Expand Down Expand Up @@ -165,6 +171,12 @@ private class DynamicObjectEncoder(

private fun isNotStructured() = result === NoOutputMark

override fun <T> encodeSerializableValue(serializer: SerializationStrategy<T>, value: T) {
encodePolymorphically(serializer, value) {
writePolymorphic = true
}
}

override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
// we currently do not structures as map key
if (currentElementIsMapKey) {
Expand All @@ -185,6 +197,11 @@ private class DynamicObjectEncoder(
enterNode(child, newMode)
}

if (writePolymorphic) {
writePolymorphic = false
current.jsObject[json.configuration.classDiscriminator] = descriptor.serialName
}

current.index = 0
return this
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,6 @@ class DecodeFromDynamicTest {
@Serializable
data class NDWrapper(@Contextual val data: NotDefault)

@Serializable
sealed class Sealed {
@Serializable
@SerialName("one")
data class One(val string: String) : Sealed()
}

@Test
fun dynamicSimpleTest() {
val dyn = js("{a: 42}")
Expand Down Expand Up @@ -204,30 +197,4 @@ class DecodeFromDynamicTest {
)
}

@Test
@Ignore
fun parsePolymorphicDefault() {
// TODO object-based polymorphic deserialization requires additional special handling
// because the discriminator lives inside the same object which is also being decoded.

val dyn = js("""({type:"one",string:"value"})""")
val expected = Sealed.One("value")

val actual1 = Json.decodeFromDynamic(Sealed.serializer(), dyn)
assertEquals(expected, actual1)

val p = Json
val actual2 = p.decodeFromDynamic(Sealed.serializer(), dyn)
assertEquals(expected, actual2)
}

@Test
fun parsePolymorphicArray() {
val dyn = js("""(["one",{"string":"value"}])""")
val expected = Sealed.One("value")

val p = Json { useArrayPolymorphism = true }
val actual = p.decodeFromDynamic(Sealed.serializer(), dyn)
assertEquals(expected, actual)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package kotlinx.serialization.json

import kotlinx.serialization.*
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
import kotlin.test.Test
import kotlin.test.assertEquals

class DynamicPolymorphismTest {
@Serializable
sealed class Sealed(val intField: Int) {
@Serializable
@SerialName("object")
object ObjectChild : Sealed(0)

@Serializable
@SerialName("data_class")
data class DataClassChild(val name: String) : Sealed(1)

@Serializable
@SerialName("type_child")
data class TypeChild(val type: String) : Sealed(2)
}

@Serializable
data class CompositeClass(val mark: String, val nested: Sealed)

@Serializable
data class AnyWrapper(@Polymorphic val any: Any)

@Serializable
@SerialName("string_wrapper")
data class StringWrapper(val text: String)

private val arrayJson = Json {
useArrayPolymorphism = true
}

private val objectJson = Json {
useArrayPolymorphism = false
}

@Test
fun testDiscriminatorName() {
val newClassDiscriminator = "key"

val json = Json {
useArrayPolymorphism = false
classDiscriminator = newClassDiscriminator
}

val value = Sealed.TypeChild("discriminator-test")
encodeAndDecode(Sealed.serializer(), value, json) {
assertEquals("type_child", this[newClassDiscriminator])
assertEquals(value.type, this.type)
assertEquals(value.intField, this.intField)
assertEquals(3, fieldsCount(this))
}
}

@Test
fun testComposite() {
val nestedValue = Sealed.DataClassChild("child")
val value = CompositeClass("composite", nestedValue)
encodeAndDecode(CompositeClass.serializer(), value, objectJson) {
assertEquals(value.mark, this.mark)
val nested = this.nested
assertEquals("data_class", nested.type)
assertEquals(nestedValue.name, nested.name)
assertEquals(nestedValue.intField, nested.intField)
assertEquals(3, fieldsCount(nested))
}

encodeAndDecode(CompositeClass.serializer(), value, arrayJson) {
assertEquals(value.mark, this.mark)
assertEquals("data_class", this.nested[0])
val nested = this.nested[1]
assertEquals(nestedValue.name, nested.name)
assertEquals(nestedValue.intField, nested.intField)
assertEquals(2, fieldsCount(nested))
}
}


@Test
fun testDataClass() {
val value = Sealed.DataClassChild("data-class")

encodeAndDecode(Sealed.serializer(), value, objectJson) {
assertEquals("data_class", this.type)
assertEquals(value.name, this.name)
assertEquals(value.intField, this.intField)
assertEquals(3, fieldsCount(this))
}

encodeAndDecode(Sealed.serializer(), value, arrayJson) {
assertEquals("data_class", this[0])
val dynamicValue = this[1]
assertEquals(value.name, dynamicValue.name)
assertEquals(value.intField, dynamicValue.intField)
assertEquals(2, fieldsCount(dynamicValue))
}
}

@Test
fun testObject() {
val value = Sealed.ObjectChild
encodeAndDecode(Sealed.serializer(), value, objectJson) {
assertEquals("object", this.type)
assertEquals(1, fieldsCount(this))
}

encodeAndDecode(Sealed.serializer(), value, arrayJson) {
assertEquals("object", this[0])
assertEquals(0, fieldsCount(this[1]))
}
}

@Test
fun testAny() {
val serializersModule = SerializersModule {
polymorphic(Any::class) {
subclass(StringWrapper.serializer())
}
}

val json = Json(objectJson) { this.serializersModule = serializersModule }

val anyValue = StringWrapper("some text")
val value = AnyWrapper(anyValue)

encodeAndDecode(AnyWrapper.serializer(), value, json) {
assertEquals("string_wrapper", this.any.type)
assertEquals(anyValue.text, this.any.text)
assertEquals(1, fieldsCount(this))
assertEquals(2, fieldsCount(this.any))
}

val json2 = Json(arrayJson) { this.serializersModule = serializersModule }

encodeAndDecode(AnyWrapper.serializer(), value, json2) {
assertEquals("string_wrapper", this.any[0])
assertEquals(anyValue.text, this.any[1].text)
assertEquals(1, fieldsCount(this))
assertEquals(2, fieldsCount(this.any))
}
}

private inline fun fieldsCount(dynamic: dynamic): Int {
return js("Object").keys(dynamic).length as Int
}

private fun <T> encodeAndDecode(deserializer: KSerializer<T>, value: T, json: Json, assertBlock: dynamic.() -> Unit) {
val dynamic = json.encodeToDynamic(deserializer, value)
assertBlock(dynamic)
val decodedValue = json.decodeFromDynamic(deserializer, dynamic)
assertEquals(value, decodedValue)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ class EncodeToDynamicTest {

@Test
fun sealed() {
// test of sealed class but not polymorphic serialization
assertDynamicForm(Sealed.One("one"))
}

Expand Down