jackson-module-kotlin icon indicating copy to clipboard operation
jackson-module-kotlin copied to clipboard

InvalidDefinitionException in case of deserialization xml to data class with @JacksonXmlText annotation

Open proton72 opened this issue 6 years ago • 16 comments

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.

proton72 avatar Mar 15 '18 18:03 proton72

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 ? )

marcvanandel avatar Mar 24 '18 10:03 marcvanandel

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.

cowtowncoder avatar Mar 24 '18 20:03 cowtowncoder

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

azihsoyn avatar Jun 22 '18 03:06 azihsoyn

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)

apatrida avatar Jul 14 '18 21:07 apatrida

@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.

TWiStErRob avatar Jul 15 '18 11:07 TWiStErRob

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.

TWiStErRob avatar Jul 15 '18 12:07 TWiStErRob

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 avatar Jul 15 '18 12:07 TWiStErRob

@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.

apatrida avatar Jul 17 '18 15:07 apatrida

this is

When xmlMapper.writeValueAsString com.fasterxml.jackson.databind.JsonMappingException: lateinit property value has not been initialized

But xmlMapper.readValue it work!

ButSoft avatar Oct 24 '18 09:10 ButSoft

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.

SteveZhangBit avatar Nov 21 '19 01:11 SteveZhangBit

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

ctruchi avatar Jan 20 '20 20:01 ctruchi

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
)

TWiStErRob avatar Jan 21 '20 01:01 TWiStErRob

@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 ("").

TWiStErRob avatar Jan 21 '20 01:01 TWiStErRob

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)

jeltz avatar Oct 26 '21 13:10 jeltz

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

ssuchkov avatar Feb 18 '22 07:02 ssuchkov

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.

Patimo avatar Mar 09 '22 09:03 Patimo