androidkotlinjacksonretrofit2jackson-dataformat-xml

How to parse this rare XML structure with Retrofit?


I'm using Retrofit and Jackson with XML parser to transform this XML into data classes. As you can see it's a very rare array inside child_group... there is one item and one info, one item and one info, etc... not a normal array.

<?xml version="1.0" encoding="utf-8"?>
<parent id="2261">
    <child_group>
        <item>
            <line>1</line>
        </item>
        <info></info>
        <item>
            <line>2</line>
        </item>
        <info></info>
        <item>
            <line>3</line>
        </item>
        <info></info>
    </child_group>
</parent>

I can't find any info about how to create the data class structure to parse this. What can I try next?

This data class structure passed to retrofit only parses the first item and the first info, but not the rest of them:

@JacksonXmlRootElement(localName = "parent")
data class Parent(
    val id: String,
    @JacksonXmlElementWrapper(useWrapping = false)
    @JacksonXmlProperty(localName = "child_group")
    val child_group: ChildGroup
)

data class ChildGroup(
    @JacksonXmlElementWrapper(useWrapping = false)
    @JacksonXmlProperty(localName = "item")
    val item: List<Item>,
    val info: String? = null
)

data class Item(
    val line: String
)

This is my Retrofit builder:

Retrofit.Builder().baseUrl("https://www.website.es/")
.addConverterFactory(
    JacksonConverterFactory.create(XmlMapper()
    .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
    .registerKotlinModule()))
.build().create(ApiService::class.java)

I tried also doing this, with same result:

@JacksonXmlRootElement(localName = "parent")
data class Parent(
    val id: String,
    @JacksonXmlProperty(localName = "child_group")
    @JacksonXmlElementWrapper(useWrapping = false)
    val childGroup: List<ChildGroupItem> 
)

data class ChildGroupItem(
    val item: Item,
    val info: String? = null 
)

data class Item(
    val line: String
)

Solution

  • This problem is similar to https://stackoverflow.com/a/79388160/860227. If you manage to sort all items in child_group (all item tags come first, followed by info), your solution will work.

    You need to create a custom deserializer for ChildGroup class like:

    class CustomDeserializer : JsonDeserializer<ChildGroup>() {
        override fun deserialize(parser: JsonParser, context: DeserializationContext): ChildGroup {
            val items = mutableListOf<Item>()
            val infos = mutableListOf<String>()
    
            val node = parser.codec.readTree<JsonNode>(parser)
            node.fields().forEach { (key, value) ->
                when (key) {
                    "item" -> {
                        if (value.isArray) {
                            value.forEach { itemNode ->
                                val item = parser.codec.treeToValue(itemNode, Item::class.java)
                                items.add(item)
                            }
                        } else {
                            val item = parser.codec.treeToValue(value, Item::class.java)
                            items.add(item)
                        }
                    }
                    "info" -> {
                        if (value.isArray) {
                            value.forEach { itemNode ->
                                infos.add(itemNode.asText())
                            }
                        } else {
                            infos.add(value.asText())
                        }
                    }
                }
            }
            return ChildGroup(items, infos)
        }
    }
    

    and register it in the data class:

    @JacksonXmlRootElement(localName = "parent")
    data class Parent(
        val id: String,
        @JsonDeserialize(using = CustomDeserializer::class) // <-- this
        val child_group: ChildGroup
    )
    
    data class ChildGroup(
        @JacksonXmlElementWrapper(useWrapping = false)
        @JacksonXmlProperty(localName = "item")
        val item: List<Item>,
        val info: List<String>,
    )
    
    data class Item(
        val line: String
    )