Reputation: 31
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:
@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
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 ofelements
gets added to the creator parameter, and insteadJacksonXmlProperty
ends up on the parameter which is the wrong name. So then the mapper tries to pass inelements
but the parameter appears to be propertyelement
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
lateinit var
with a private setterhashCode()
, 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