Marek Materzok
Marek Materzok

Reputation: 31

Problem with JacksonXmlElementWrapper on Kotlin

I'm trying to parse a XML file using Jackson and Kotlin on Android Studio. In particular, I want to run the code from the following test case in jackson-module-kotlin:

https://github.com/FasterXML/jackson-module-kotlin/blob/master/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/Github153.kt

    @JacksonXmlRootElement(localName = "MyPojo")
    data class MyDataPojo (
            @JacksonXmlElementWrapper(localName = "elements")
            @JacksonXmlProperty(localName = "element")
            val elements : List<MyDataElement>
    )

    data class MyDataElement (
            @JacksonXmlProperty(localName = "value", isAttribute = true)
            var value: String
    )

I'm trying to parse a XML file like this:

    val xmlMapper = XmlMapper().apply {
        registerModule(KotlinModule())
    }
    val pojo = context.resources.assets.open(name).use { input ->
        xmlMapper.readValue<MyDataPojo>(input)
    }

This fails with the following exception:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Invalid definition for property `elements` (of type `eu.tilk.wihajster.MyDataPojo`): Could not find creator property with name 'elements' (known Creator properties: [element])

I'm using Jackson 2.10.3, here is my dependencies section from build.gradle:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'com.fasterxml.jackson.core:jackson-core:2.10.3'
    implementation 'com.fasterxml.jackson.core:jackson-annotations:2.10.3'
    implementation 'com.fasterxml.jackson.core:jackson-databind:2.10.3'
    implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.10.3'
    implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.10.3"
    implementation 'javax.xml.stream:stax-api:1.0-2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

What I'm doing wrong? This code is copied from a test for jackson-module-kotlin, so I believe it should work fine.

Upvotes: 3

Views: 1242

Answers (1)

khreenberg
khreenberg

Reputation: 53

This is an open issue with the Jackson Kotlin Module:

apatrida commented on 14 Jul 2018
JacksonXmlElementWrapper cannot be on a parameter therefore no property name of elements gets added to the creator parameter, and instead JacksonXmlProperty ends up on the parameter which is the wrong name. So then the mapper tries to pass in elements but the parameter appears to be property element

The test you have linked is actually expected to fail for data classes:

@JacksonXmlRootElement(localName = "MyPojo")
data class MyDataPojo (
        @JacksonXmlElementWrapper(localName = "elements")
        @JacksonXmlProperty(localName = "element")
        val elements : List<MyDataElement>
)

@Test
// Conflict between the annotations that is not current resolvable.
fun test_data_class() {
    expectFailure<InvalidDefinitionException>("Problem with conflicting annotations related to #153 has been fixed!") {
        // I create a pojo from the xml using the data classes
        val pojoFromXml = mapper.readValue(xml, MyDataPojo::class.java)

        // I create a xml from the pojo
        val xmlFromPojo = mapper.writeValueAsString(pojoFromXml)

        // I compare the original xml with the xml generated from the pojo
        assertEquals(xml, xmlFromPojo)
    }
}

It is however expected to work for non-data classes:

@JacksonXmlRootElement(localName = "MyPojo")
class MyPojo {
    @JacksonXmlElementWrapper(localName = "elements")
    @JacksonXmlProperty(localName = "element")
    var list: List<MyElement>? = null
}

@Test
fun test_class() {
    // I create a pojo from the xml using the standard classes
    val pojoFromXml = mapper.readValue(xml, MyPojo::class.java)

    // I create a xml from the pojo
    val xmlFromPojo = mapper.writeValueAsString(pojoFromXml)

    // I compare the original xml with the xml generated from the pojo
    assertEquals(xml, xmlFromPojo)
}

As you can see—even though Kotlin makes it a little difficult—the difference is that elements is a constructor parameter in the data class, but a field in the regular class.

The best workaround I can think of without creating custom (de)serializers is

  • Making the primary constructor private
  • Letting the field be lateinit var with a private setter
  • Manually overriding hashCode(), equals() and toString()

For example:

@JacksonXmlRootElement(localName = "MyPojo")
class WorkaroundPojo private constructor() {
    constructor(list: List<MyElement>) : this() {
        this.list = list
    }

    @JacksonXmlElementWrapper(localName = "elements")
    @JacksonXmlProperty(localName = "element")
    lateinit var list: List<MyElement>
        private set

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as WorkaroundPojo

        if (list != other.list) return false

        return true
    }

    override fun hashCode(): Int {
        return list.hashCode()
    }

    override fun toString(): String {
        return "WorkaroundPojo(list=$list)"
    }
}

@Test
fun test_workaround() {
    val pojoFromXml = mapper.readValue(xml, WorkaroundPojo::class.java)
    val xmlFromPojo = mapper.writeValueAsString(pojoFromXml)
    assertEquals(xml, xmlFromPojo)
}

Here is a JUnit5 test with a more complex example showing how to combine the above with inheritance, enums and more using Jackson 2.13.0:

import com.fasterxml.jackson.databind.SerializationFeature
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.jaxb.JaxbAnnotationModule
import com.fasterxml.jackson.module.kotlin.kotlinModule
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import javax.xml.bind.annotation.XmlEnumValue

class StackOverflow {

    abstract class ActivatableElement {
        abstract val active: Boolean
    }

    class ComplexExample private constructor(
        override val active: Boolean = false,
        val myEnum: MyEnum = MyEnum.First,
    ) : ActivatableElement() {

        constructor(active: Boolean, myEnum: MyEnum, myElements: List<MyElement>) : this(active, myEnum) {
            this.elements = myElements
        }

        @JacksonXmlElementWrapper(localName = "elementList")
        @JacksonXmlProperty(localName = "element")
        lateinit var elements: List<MyElement>
            private set

        data class MyElement(
            val name: String,
            val number: Int
        )

        enum class MyEnum {
            @XmlEnumValue("First value of MyEnum")
            First,

            @XmlEnumValue("Second value of MyEnum")
            Second,
        }

        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (javaClass != other?.javaClass) return false

            other as ComplexExample

            if (active != other.active) return false
            if (myEnum != other.myEnum) return false
            if (elements != other.elements) return false

            return true
        }

        override fun hashCode(): Int {
            var result = active.hashCode()
            result = 31 * result + myEnum.hashCode()
            result = 31 * result + elements.hashCode()
            return result
        }
    }

    val xml = """
        <ComplexExample>
          <active>true</active>
          <myEnum>Second value of MyEnum</myEnum>
          <elementList>
            <element>
              <name>First Item</name>
              <number>1</number>
            </element>
            <element>
              <name>Second Item</name>
              <number>2</number>
            </element>
            <element>
              <name>Third Item</name>
              <number>3</number>
            </element>
          </elementList>
        </ComplexExample>
    """.trimIndent()

    @Test
    fun test_complex_example() {
        val mapper = XmlMapper.builder()
            .addModule(kotlinModule())
            .addModule(JaxbAnnotationModule())
            .build()
            .enable(SerializationFeature.INDENT_OUTPUT)

        val example = mapper.readValue(xml, ComplexExample::class.java)
        val xmlFromExample = mapper.writeValueAsString(example).trim()
        Assertions.assertEquals(xml, xmlFromExample)

        val expected = ComplexExample(
            active = true,
            myEnum = ComplexExample.MyEnum.Second,
            myElements = listOf(
                ComplexExample.MyElement("First Item", 1),
                ComplexExample.MyElement("Second Item", 2),
                ComplexExample.MyElement("Third Item", 3),
            )
        )
        Assertions.assertEquals(expected, example)
        Assertions.assertEquals(expected.hashCode(), example.hashCode())
    }
}

Upvotes: 2

Related Questions