jackson-module-kotlin
jackson-module-kotlin copied to clipboard
InvalidDefinitionException in case of deserialization xml to data class with @JacksonXmlText annotation
Data class example:
@JacksonXmlRootElement(localName = "sms")
@JsonIgnoreProperties(ignoreUnknown = true)
data class Sms(
@set:JacksonXmlProperty(localName = "Phone", isAttribute = true)
var phone: String?,
@JacksonXmlText
var text: String? = ""
)
Corresponding xml:
<sms Phone="435242423412" Id="43234324">Lorem ipsum</sms>
Deserialization causes such output:
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Invalid definition for property `` (of type `proton72.github.com.Sms`): Could not find creator property with name '' (known Creator properties: [Phone, text])
at [Source: (StringReader); line: 1, column: 1]
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:62)
at com.fasterxml.jackson.databind.DeserializationContext.reportBadPropertyDefinition(DeserializationContext.java:1446)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.addBeanProps(BeanDeserializerFactory.java:567)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.buildBeanDeserializer(BeanDeserializerFactory.java:227)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.createBeanDeserializer(BeanDeserializerFactory.java:137)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer2(DeserializerCache.java:411)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer(DeserializerCache.java:349)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:264)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:244)
at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:142)
at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:477)
at com.fasterxml.jackson.databind.ObjectMapper._findRootDeserializer(ObjectMapper.java:4178)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3997)
Reproduced on jackson-dataformat-xml and jackson-module-kotlin 2.9.4 both.
Is this an issue in jackson-module-kotlin
or should this be in jackson-binding
? Debugging this issue (in my own set up hitting the same exception) it seems that in the BeanDeserializerFactory
line 565 the match between prop.getName()
and creatorProp.getName()
fails. This results into an exception 'cause there's no creator prop for this property ...
(maybe report this in https://github.com/FasterXML/jackson-databind/issues ? )
Good question. If it is possible to reproduce the problem with just Java, it belongs either on XML module or jackson-databind
. I would guess it would probably be XML-specific.
I have the same problem.
And I found a workaround to avoid the exception. https://stackoverflow.com/questions/47122736/pojo-object-for-this-xml-response-in-kotlin
I am not sure why this fails, it bombs outside of the module @cowtowncoder ... I commited a test case that is marked ignored for now, but fails with this exception.
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Invalid definition for property `` (of type `com.fasterxml.jackson.module.kotlin.test.TestGithub138$Sms`): Could not find creator property with name '' (known Creator properties: [Phone, text])
at [Source: (StringReader); line: 1, column: 1]
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:62)
at com.fasterxml.jackson.databind.DeserializationContext.reportBadPropertyDefinition(DeserializationContext.java:1446)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.addBeanProps(BeanDeserializerFactory.java:567)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.buildBeanDeserializer(BeanDeserializerFactory.java:227)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.createBeanDeserializer(BeanDeserializerFactory.java:137)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer2(DeserializerCache.java:411)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer(DeserializerCache.java:349)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:264)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:244)
at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:142)
at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:477)
at com.fasterxml.jackson.databind.ObjectMapper._findRootDeserializer(ObjectMapper.java:4190)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4009)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3023)
at com.fasterxml.jackson.module.kotlin.test.TestGithub138.testDeserProblem(Github138.kt:34)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
@cowtowncoder To move this a bit forward...
Here's a base repro:
package test
fun main(args: Array<String>) {
val mapper = XmlMapper().apply {
registerModule(KotlinModule())
configure(FAIL_ON_UNKNOWN_PROPERTIES, false)
}
val feed = mapper.readValue<Feed>(
"""
<feed>
<attributes>
<attribute code="private">Private Event</attribute>
<attribute code="public">Public Event</attribute>
</attributes>
</feed>
""".trimIndent()
)
println(feed)
}
@JacksonXmlRootElement(localName = "feed")
data class Feed(
@JacksonXmlElementWrapper(localName = "attributes")
val attributes: List<Attribute>
)
data class Attribute(
@JacksonXmlProperty(isAttribute = true)
val code: String,
@JacksonXmlText
val title: String?
)
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Invalid definition for property `` (of type `test.Attribute`): Could not find creator property with name '' (known Creator properties: [code, title])
at [Source: (StringReader); line: 1, column: 1]
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:62)
at com.fasterxml.jackson.databind.DeserializationContext.reportBadPropertyDefinition(DeserializationContext.java:1446)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.addBeanProps(BeanDeserializerFactory.java:567)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.buildBeanDeserializer(BeanDeserializerFactory.java:227)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.createBeanDeserializer(BeanDeserializerFactory.java:137)
removing @JacksonXmlText
works as expected, but obviously doesn't deserialize the text:
Feed(attributes=[Attribute(code=private, title=null), Attribute(code=public, title=null)])
Commenting/removing the Kotlin Attribute class and using this instead works:
package test;
public class Attribute {
@JacksonXmlProperty(isAttribute = true)
public String code;
@JacksonXmlText
public String title;
@Override public String toString() { return String.format("Attribute(%s, %s)", code, title); }
}
Feed(attributes=[Attribute(private, Private Event), Attribute(public, Public Event)])
Getting a bit more close to Kotlin data classes fails the same way as Kotlin:
public class Attribute {
@JacksonXmlProperty(isAttribute = true)
private final String code;
@JacksonXmlText
private final String title;
public Attribute(
@JacksonXmlProperty(localName = "code") String code,
@JacksonXmlProperty(localName = "title") String title) {
this.code = code;
this.title = title;
}
@Override public String toString() {
return String.format("Attribute(%s, %s)", code, title);
}
}
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Invalid definition for property `` (of type `test.Attribute`): Could not find creator property with name '' (known Creator properties: [code, title])
at [Source: (StringReader); line: 1, column: 1]
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:62)
at com.fasterxml.jackson.databind.DeserializationContext.reportBadPropertyDefinition(DeserializationContext.java:1446)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.addBeanProps(BeanDeserializerFactory.java:567)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.buildBeanDeserializer(BeanDeserializerFactory.java:227)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.createBeanDeserializer(BeanDeserializerFactory.java:137)
Note: removing registerModule(KotlinModule())
makes no difference.
If we solve the Java version to work (maybe the annotations are wrong), we could figure out how to do the same in Kotlin.
Note the actual Kotlin-compiled code looks like this:
public final class Attribute {
private final String code;
@JacksonXmlText
private final String title;
public Attribute(
@JacksonXmlProperty(isAttribute = true) @NotNull String code,
@Nullable String title) {
this.code = code;
this.title = title;
}
// data class junk (get,toString,hashCode,equals,componentN,copy) and nullability annotations omitted
}
Revelation: @JacksonXmlText
cannot be applied to a constructor parameter! This means that Kotlin cannot put the attribute in the right place for it to be picked up by the definition builder to make use of the constructor.
Outdated, I recommend to use setXMLTextElementName
.
Based on the above discovery here's another workaround (instead of using JsonNode):
data class Attribute(
@JacksonXmlProperty(isAttribute = true)
val code: String
) {
@JacksonXmlText
lateinit var title: String private set
override fun toString() = "$code->$title"
}
Feed(attributes=[private->Private Event, public->Public Event])
Note: this is not a solution as the field will be writable (var
, private set
helps a bit, but still), will have no construct-time validation that the field is set (lateinit
), and the field will not play in data class
operations like equals
, toString
, and others (it's not in the primary constructor)!
@TWiStErRob you could use a delegate that only allows the property to be written once which would not give you compile time read only check but would at least prevent it from being set again.
When xmlMapper.writeValueAsString com.fasterxml.jackson.databind.JsonMappingException: lateinit property value has not been initialized
But xmlMapper.readValue it work!
I find that we can manually create a second constructor and set it to JsonCreator. It looks like
data class Action(
@JacksonXmlProperty(isAttribute = true, localName = "humanaction")
val humanAction: String? = null,
@JacksonXmlProperty(isAttribute = true, localName = "localvariable")
val localVariable: String? = null,
@JacksonXmlText
val value: String? = null
) {
@JsonCreator
constructor(humanAction: String?, localVariable: String?) : this(humanAction, localVariable, null)
}
In my case, it works both for serialization and deserialization.
I just found someone on kotlin slack (named Stephan Schroeder) providing a solution :
You need a mapper like this :
XmlMapper(JacksonXmlModule()
.apply {
setXMLTextElementName("text")
})
and annotate your field like this:
data class Attribute(
@JacksonXmlProperty(isAttribute = true)
val code: String,
@JsonProperty("text")
@JacksonXmlText
val title: String?
)
This works for me with jackson 2.10.1. Slack user claims it works with jackson 2.9.9
Genius, it works for me too! Thank you very much for sharing @ctruchi!
Note @ctruchi @JacksonXmlText
doesn't have an effect with setXMLTextElementName
when deserializing only. However it is required for serialization.
Following @ctruchi's example this also works, with the benefit of not mixing XML and JSON annotations:
XmlMapper(JacksonXmlModule().apply {
setXMLTextElementName("innerText")
})
data class Attribute(
@JacksonXmlProperty(isAttribute = true)
val code: String,
@JacksonXmlProperty(localName = "innerText") // for deserialization
@JacksonXmlText // for serialization
val title: String
)
@FasterXML team
The default XMLTextElementName is ""
, but when @JsonProperty("")
is encountered it actually substitutes to the name of the Kotlin property. There seems to be a conflict between:
public class JacksonXmlModule {
protected String _cfgNameForTextElement = FromXmlParser.DEFAULT_UNNAMED_TEXT_PROPERTY;
public class FromXmlParser {
/**
* The default name placeholder for XML text segments is empty
* String ("").
*/
public final static String DEFAULT_UNNAMED_TEXT_PROPERTY = "";
and
public @interface JsonProperty {
/**
* Special value that indicates that handlers should use the default
* name (derived from method or field name) for property.
*
* @since 2.1
*/
public final static String USE_DEFAULT_NAME = "";
/**
* Defines name of the logical property, i.e. JSON object field
* name to use for the property. If value is empty String (which is the
* default), will try to use name of the field that is annotated.
* Note that there is
* <b>no default name available for constructor arguments</b>,
* meaning that
* <b>Empty String is not a valid value for constructor arguments</b>.
*/
String value() default USE_DEFAULT_NAME;
or
public @interface JacksonXmlProperty {
String localName() default "";
and because of this conflict it's not possible to reference the "XML text element" by its default name (""
).
If I am not mistaken to proposed workaround from @ctruchi does not work if there are no attributes on the element or if the attributes are optional. It seems like the workaround only works with at least one attribute in the XML.
data class Tag1(
@JacksonXmlProperty(isAttribute = true)
val attr: String,
@JacksonXmlProperty(localName = "_text")
val text: String,
)
data class Tag2(
@JacksonXmlProperty(localName = "_text")
val text: String,
)
fun parseTest1(xml: String) {
val mapper = XmlMapper(JacksonXmlModule().apply { setXMLTextElementName("_text") }).apply {
registerModule(kotlinModule {})
}
val t: Tag1 = mapper.readValue(xml)
println(t)
}
fun parseTest2(xml: String) {
val mapper = XmlMapper(JacksonXmlModule().apply { setXMLTextElementName("_text") }).apply {
registerModule(kotlinModule {})
}
val t: Tag2 = mapper.readValue(xml)
println(t)
}
fun main() {
parseTest1("""<tag1 attr="bar">foo</tag1>""".trimIndent())
parseTest2("""<tag2>foo</tag2>""".trimIndent())
}
Tag1(attr=bar, text=foo)
Exception in thread "main" com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `se.biobanksverige.nbrsamples.Tag2` (although at least one Creator exists): no default no-arguments constructor found
at [Source: (StringReader); line: 1, column: 10]
at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1728)
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1353)
at com.fasterxml.jackson.databind.deser.ValueInstantiator.createUsingDefault(ValueInstantiator.java:248)
at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createUsingDefault(StdValueInstantiator.java:275)
at com.fasterxml.jackson.dataformat.xml.deser.XmlTextDeserializer.deserialize(XmlTextDeserializer.java:92)
at com.fasterxml.jackson.dataformat.xml.deser.XmlDeserializationContext.readRootValue(XmlDeserializationContext.java:91)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4675)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3630)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3613)
at se.biobanksverige.nbrsamples.MainKt.parseTest2(main.kt:203)
at se.biobanksverige.nbrsamples.MainKt.main(main.kt:189)
at se.biobanksverige.nbrsamples.MainKt.main(main.kt)
Hey guys
After jackson-dataformat-xml
(and all related Jackson libraries) update to 2.13.1 I've used XmlMapper.Builder#nameForTextElement
for setting up the xml text element name and this problem we had here before is reproduced on Kotlin data classes.
If I use mapper initialised like
XmlMapper(
JacksonXmlModule()
.apply { setXMLTextElementName("innerText") }
).registerKotlinModule()
it works fine.
If I use
XmlMapper.builder()
.nameForTextElement("innerText")
.build()
.registerKotlinModule()
I have Invalid definition for property ''
for fields annotated @JacksonXmlText
and @JacksonXmlProperty
simultaneously.
I assume the problem is JacksonXmlModule._cfgNameForTextElement
is not set in case of Builder configuration.
My workaround is detaching reading part from the writing one by declaring the function like this
data class Foo(
@JacksonXmlProperty(localName = "innerText")
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
val value: Float,
) {
@JacksonXmlText
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
fun xmlText() = value
}
but I'd rather stack to less sophisticated solution
I was using the innerText
workaround mentioned above but after updating to a newer version of the library, this no longer worked for me either.
Using version 2.12.6 of jackson-dataformat-xml
and jackson-module-kotlin
I had to move the property out of the constructor (and make it a nullable var), as a result had to stop using a data class as these require at least one constructor parameter. Using the @JacksonXmlText
meant I didn't need the innerText
anymore. This was for deserializing only as this was all I required.
For this XML:
<statusResponse>3</statusResponse>
The following worked for me:
@JacksonXmlRootElement(localName = "statusResponse")
class ResponseChangeStatus(): {
@JacksonXmlText()
var status : Short? = null
}
I was still able to use the builder method for creating the XmlMapper, without the need for nameForTextElement("innerText")
.
XmlMapper.builder()
.build()
.registerKotlinModule()
Haven't tried this on 2.13.x and above versions yet.