xmlutil icon indicating copy to clipboard operation
xmlutil copied to clipboard

Values serialized as collections via custom serializers aren't decoded correctly

Open YarnSphere opened this issue 1 year ago • 2 comments
trafficstars

Description

Consider the following toy example:

@Serializable
data class User(val name: String, val shoppingCart: ShoppingCart)

@Serializable(with = ShoppingCartSerializer::class)
data class ShoppingCart(val items: MutableList<Item>) {
    val total: Double get() = items.sumOf { it.price }
}

@Serializable
data class Item(val name: String, val price: Double)

Where ShoppingCartSerializer is a custom serializer, which serializes ShoppingCart as a list:

class ShoppingCartSerializer : KSerializer<ShoppingCart> {
    private val listSerializer = ListSerializer(Item.serializer())

    override val descriptor: SerialDescriptor =
        SerialDescriptor("org.example.ShoppingCart", listSerializer.descriptor)

    override fun serialize(encoder: Encoder, value: ShoppingCart): Unit =
        encoder.encodeSerializableValue(listSerializer, value.items)

    override fun deserialize(decoder: Decoder): ShoppingCart =
        ShoppingCart(decoder.decodeSerializableValue(listSerializer).toMutableList())
}

The following test, which should pass, does not:

val data = User(
    "Alice",
    ShoppingCart(mutableListOf(Item("T-Shirt", 20.0), Item("Boots", 50.0)))
)

@Test
fun testCustomCollectionXmlSerialization() {
    val encodedXml = XML.encodeToString(data)
    val decodedXml = XML.decodeFromString<User>(encodedXml)

    assertEquals(
        """<User name="Alice"><Item name="T-Shirt" price="20.0"/><Item name="Boots" price="50.0"/></User>""",
        encodedXml
    )
    assertEquals(data, decodedXml)
}

The first assert passes, i.e., encoding is correct. The second assert, however, fails with:

Expected :User(name=Alice, shoppingCart=ShoppingCart(items=[Item(name=T-Shirt, price=20.0), Item(name=Boots, price=50.0)]))
Actual   :User(name=Alice, shoppingCart=ShoppingCart(items=[Item(name=Boots, price=50.0)]))

I.e., decoding drops all items of the collection but the last.

Notes

  • Using a standard "collection" (List, Set, Array, etc.) for the shopping cart directly instead of a class with a delegated serializer results in the decoding working as expected.
  • Changing the custom serializer's implementation to delegate to SetSerializer or any other "built-in" collection serializer results in the same incorrect behaviour.
  • The custom serializer's deserialize function is called once for each item of the collection, instead of only being called once for the whole collection.
  • The kotlinx.serialization JSON serializer has no problems with this example.

Reproduction

The code above showcasing the issue is available at: https://github.com/YarnSphere/xmlutil-custom-collection-serialization

Versions tested

  • 0.86.3 with Kotlin 1.9.24
  • 0.90.0-RC2 with Kotiln 2.0.0

YarnSphere avatar Jun 08 '24 07:06 YarnSphere