Search code examples
javaxmlkotlinjacksonjackson-dataformat-xml

XML Jackson binding with a not really nested XML in Kotlin


I have "a not nice looking" XML. (yes it's valid but different to the ones I normally get and where I know how to deal with it)

Normally I wanted so see something like:

<Parent....>
  <Books>
    <Book ...>
    <Book ...>
    <Book ...>
  </Books>
</Parent> 

my Data Class for the abvoe XML would look like this:

data class Parent(
    @JacksonXmlProperty(localName = "Books")
    @JacksonXmlElementWrapper
    var books: List<Book> 
)

data class Book(
    .....
    )

but unfortunately our incoming XML looks like this:

<Parent....>
    <Book ...>
    <Something>
    <Book ....>
    <Something>
    <Book ...>
</parent> 

So I'm struggling with matching this with a normal Jackson Mapper, without using a custom one. (and if I need a custom one, how it needs to look like?)

My mapper looks like this:

val kotlinModule: KotlinModule = KotlinModule.Builder()
                .strictNullChecks(false)
                .nullIsSameAsDefault(true) // needed, else it will break for null https://github.com/FasterXML/jackson-module-kotlin/issues/130#issuecomment-546625625
                .build()

val xmlMapper =
                XmlMapper(JacksonXmlModule())
                        .registerModule(kotlinModule)
                        .registerModule(JavaTimeModule())
                        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) // to parse the dates as LocalDate, else parsing error
                        .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)
                        .enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)
                        .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)

Solution

  • For deserializing such an XML there's two tricks you can use:

    • Explicitly turning off wrapping for your List<Book>, as they are sitting directly in the Parent
    • Working around engine limitations with a custom setter, a solution proposed in GitHub issue #275

    More specifically, you can annotate your classes as such to allow deserialization of your example:

    @JacksonXmlRootElement
    class Parent {
        @JacksonXmlProperty(localName = "Book")
        @JacksonXmlElementWrapper(useWrapping = false)
        var books: List<Book> = ArrayList()
            set(value) {
                field = books + value
            }
    }
    
    data class Book(
        @JacksonXmlProperty
        val title: String
    )
    

    I uploaded a full example, which uses your Jackson config and sample XML, as a Kotlin Script to GitHub Gist. It also shows the output of the deserialized XML at the bottom.