FasterXML / jackson-dataformat-xml

Extension for Jackson JSON processor that adds support for serializing POJOs as XML (and deserializing from XML) as an alternative to JSON
Apache License 2.0
565 stars 221 forks source link

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

Open gapag opened 2 years ago

gapag commented 2 years ago

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

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

cowtowncoder commented 2 years ago

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.