jackson-dataformat-xml icon indicating copy to clipboard operation
jackson-dataformat-xml copied to clipboard

Kotlin: `@Json(De|S)erialize(converter= *)` exhibits different behaviour when deserializing Json or XML

Open gapag opened this issue 4 years ago • 1 comments

Using gradle, I include:

    val jackson_version = 2.13.0
    implementation("com.fasterxml.jackson.core:jackson-databind:$jackson_version")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version")
    implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:$jackson_version")
    implementation("com.fasterxml.woodstox:woodstox-core:6.2.5")

Problem

  • I want to serialize a map as a list, and deserialize it as a map to minimize repetition
  • Therefore I use @JsonDeserialize and @JsonSerialize using converter=
  • Serializing in Json and XML works as expected
  • Deserializing does not: Jackson
    • can read back Json
    • cannot correctly read back XML (reads an empty map)

Test code

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.databind.util.StdConverter
import com.fasterxml.jackson.dataformat.xml.XmlMapper
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty
import com.fasterxml.jackson.module.kotlin.KotlinModule
import java.io.File

class SeqL2M : StdConverter<List<Seq>, Map<String, Seq>>() {
    override fun convert(value: List<Seq>?): Map<String, Seq> {
        return value?.associateBy { it.name } ?: mapOf()
    }
}

class SeqM2L : StdConverter<Map<String, Seq>, List<Seq>>() {
    override fun convert(value: Map<String, Seq>?): List<Seq> {
        return value?.values?.toList() ?: listOf()
    }
}

data class Root(
    @JacksonXmlProperty(isAttribute = true)
    val id: String,
    @JsonDeserialize(converter = SeqL2M::class)
    @JsonSerialize(converter = SeqM2L::class)
    // @JacksonXmlElementWrapper(useWrapping = false) // Makes no difference if uncommented
    val seq: Map<String, Seq>
)

data class Seq(
    @JacksonXmlProperty(isAttribute = true)
    var name: String,
)

fun main() {

    val jmapper = ObjectMapper()
    val xmapper = XmlMapper()
    listOf(jmapper, xmapper).forEach { it.registerModule(KotlinModule.Builder().build()) }
    val data = Root(id = "identifier",
        seq = "abc".map { it.toString() }.associateWith { Seq(it) }
    )

    val xcontent = File("input.xml")
    val jcontent = File("input.json")

    xmapper.writeValue(xcontent, data)
    jmapper.writeValue(jcontent, data)

    val x = xmapper.readValue(xcontent, Root::class.java)
    val j = jmapper.readValue(jcontent, Root::class.java)
    println(x.toString())
    println(j.toString())


}

Files written

input.xml

<Root id="identifier"><seq name="a"/><seq name="b"/><seq name="c"/></Root>

input.json

{"id":"identifier","seq":[{"name":"a"},{"name":"b"},{"name":"c"}]}

Stdout

Root(id=identifier, seq={})
Root(id=identifier, seq={a=Seq(name=a), b=Seq(name=b), c=Seq(name=c)})

Observations

  • First row of Stdout above shows what the problem is (seq={}, instead should be same as second line).
  • It feels like a bug since I would expect the library to read back what it wrote.
  • Yet, it looks like that on deserialization
    • the XmlMapper invokes SeqL2M::convert 3 times
      • as if it interpreted the data as three mappings/list starting on opening <seq>
    • whereas ObjectMapper invokes SeqL2M::convert only once.
  • Is, then, some further configuration on the XmlMapper missing on my side?
  • I guess this specific problem has something to do with https://github.com/FasterXML/jackson-databind/issues/1152

gapag avatar Nov 11 '21 08:11 gapag

Yes, XML backend has some oddities and can not -- for example -- necessarily handle Maps or Collections given that XML lacks such type constructs at token level. At databinding level it can use target type information to handle wrapping of lists (or not); at streaming level not.

Code you have will not work: you cannot read token stream as Lists or Maps at streaming level when working with XML. This is a fundamental limitation.

So basically you can not really make this code work without special handling for XML use case.

cowtowncoder avatar Nov 12 '21 21:11 cowtowncoder