Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XML parsing error when nested XML has element with unbound attribute #328

Closed
dwikle opened this issue Jan 24, 2019 · 6 comments
Closed

XML parsing error when nested XML has element with unbound attribute #328

dwikle opened this issue Jan 24, 2019 · 6 comments
Milestone

Comments

@dwikle
Copy link

dwikle commented Jan 24, 2019

I'm seeing a parsing error which seems to be a bug.

Using the following:

  • com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.8
  • com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.8
  • org.projectlombok:lombok:1.18.4

POJOs:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Inner {

    @XmlElement(name = "Date")
    String date;

    @XmlElement(name = "Description")
    String description;

}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Outer {
    
    @XmlElement(name = "Inner")
    Set<Inner> innerSet;
    
}

For Inner there is an attribute on the Date element, named scheduled below, which I want to ignore.
E.g.

<Inner>
    <Date scheduled="true">2016-03-18</Date>
    <Description>foo</Description>
</Inner>

If I deserialize into an instance of Inner, it works as expected. But when Inner is nested inside another element, I get an error.
E.g.

<Outer>
  <Inner>
    <Date scheduled="true">2016-03-18</Date>
    <Description>foo</Description>
  </Inner>
  <Inner>
    <Date scheduled="true">2016-03-18</Date>
    <Description>foo</Description>
  </Inner>
</Outer>

Produces this error:

com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.example.TestCase1$Inner` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('foo')
 at [Source: (StringReader); line: 1, column: 79] (through reference chain: com.example.TestCase1$Outer["Inner"]->java.util.HashSet[1])

I tried without using Lombok annotations and got the same result.

Full code to reproduce:

package com.example;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.junit.Test;

import javax.xml.bind.annotation.XmlElement;
import java.util.Set;

public class TestCase1 {

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Inner {

        @XmlElement(name = "Date")
        String date;

        @XmlElement(name = "Description")
        String description;

    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Outer {

        @XmlElement(name = "Inner")
        Set<Inner> innerSet;

    }

    @Test
    public void test1() throws Exception {

        String xmlWithAttr =
                "<Inner>" +
                "    <Date scheduled=\"true\">2016-03-18</Date>" +
                "    <Description>foo</Description>" +
                "</Inner>";

        String xmlWithoutAttr =
                "<Inner>" +
                "    <Date>2016-03-18</Date>" +
                "    <Description>foo</Description>" +
                "</Inner>";

        String nestedXmlWithAttr =
                "<Outer>" +
                "    <Inner>" +
                "        <Date scheduled=\"true\">2016-03-18</Date>" +
                "        <Description>foo</Description>" +
                "    </Inner>" +
                "    <Inner>" +
                "        <Date scheduled=\"true\">2016-03-18</Date>" +
                "        <Description>bar</Description>" +
                "    </Inner>" +
                "</Outer>";

        String nestedXmlWithoutAttr =
                "<Outer>" +
                "    <Inner>" +
                "        <Date>2016-03-18</Date>" +
                "        <Description>foo</Description>" +
                "    </Inner>" +
                "    <Inner>" +
                "        <Date>2016-03-18</Date>" +
                "        <Description>bar</Description>" +
                "    </Inner>" +
                "</Outer>";

        XmlMapper mapper = new XmlMapper();
        mapper.setDefaultUseWrapper(false);
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        JaxbAnnotationModule module = new JaxbAnnotationModule();
        mapper.registerModule(module);

        Inner inner1 = mapper.readValue(xmlWithoutAttr, Inner.class);   // works
        System.out.println(inner1);

        Inner inner2 = mapper.readValue(xmlWithAttr, Inner.class);      // works
        System.out.println(inner2);

        Outer outer1 = mapper.readValue(nestedXmlWithoutAttr, Outer.class); // works
        System.out.println(outer1);

        Outer outer2 = mapper.readValue(nestedXmlWithAttr, Outer.class);    // errors
        System.out.println(outer2);
    }

}

Output:

TestCase1.Inner(date=2016-03-18, description=foo)
TestCase1.Inner(date=2016-03-18, description=foo)
TestCase1.Outer(innerSet=[TestCase1.Inner(date=2016-03-18, description=foo), TestCase1.Inner(date=2016-03-18, description=bar)])

com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.example.TestCase1$Inner` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('foo')
 at [Source: (StringReader); line: 1, column: 91] (through reference chain: com.example.TestCase1$Outer["Inner"]->java.util.HashSet[1])

	at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
	at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1343)
	at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1032)
	at com.fasterxml.jackson.databind.deser.ValueInstantiator._createFromStringFallbacks(ValueInstantiator.java:371)
	at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createFromString(StdValueInstantiator.java:323)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromString(BeanDeserializerBase.java:1373)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:171)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:161)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:286)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:245)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:27)
	at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:127)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:288)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)
	at com.fasterxml.jackson.dataformat.xml.deser.WrapperHandlingDeserializer.deserialize(WrapperHandlingDeserializer.java:113)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4013)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3004)
	at com.example.TestCase1.test1(TestCase1.java:93)
	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
Copy link
Member

Thank you for reporting this, and including reproduction.
Just one request: I can not use any code with Lombok (there's longer explanation but for now this is the situation) for reproduction, so would it be possible to get generated code instead?
It's fine to use Lombok with Jackson, just not for reproduction part.

Aside from that, the best way to debug as user is to first see what serialization is (create value, serialize as XML), compare to XML you are trying to bind. This won't always show the problem but can help with some structural impedance.

@dwikle
Copy link
Author

dwikle commented Mar 8, 2019

@cowtowncoder Here is the code to reproduce without lombok.

package com.example;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule;
import org.junit.Test;

import javax.xml.bind.annotation.XmlElement;
import java.util.Set;

public class TestCase2 {

    public static class Inner {

        @XmlElement(name = "Date")
        String date;

        @XmlElement(name = "Description")
        String description;

        public Inner() {}

        public Inner(String date, String description) {
            this.date = date;
            this.description = description;
        }

        public String getDate() {
            return date;
        }

        public Inner setDate(String date) {
            this.date = date;
            return this;
        }

        public String getDescription() {
            return description;
        }

        public Inner setDescription(String description) {
            this.description = description;
            return this;
        }

        public String toString() {
            return "TestCase2.Inner(date=" + this.getDate() + ", description=" + this.getDescription() + ")";
        }
    }

    public static class Outer {

        @XmlElement(name = "Inner")
        Set<Inner> innerSet;

        public Outer() { }

        public Outer(Set<Inner> innerSet) {
            this.innerSet = innerSet;
        }

        public Set<Inner> getInnerSet() {
            return innerSet;
        }

        public Outer setInnerSet(Set<Inner> innerSet) {
            this.innerSet = innerSet;
            return this;
        }

        public String toString() {
            return "TestCase2.Outer(innerSet=" + this.getInnerSet() + ")";
        }
    }

    @Test
    public void test1() throws Exception {

        String xmlWithAttr =
                "<Inner>" +
                "    <Date scheduled=\"true\">2016-03-18</Date>" +
                "    <Description>foo</Description>" +
                "</Inner>";

        String xmlWithoutAttr =
                "<Inner>" +
                "    <Date>2016-03-18</Date>" +
                "    <Description>foo</Description>" +
                "</Inner>";

        String nestedXmlWithAttr =
                "<Outer>" +
                "    <Inner>" +
                "        <Date scheduled=\"true\">2016-03-18</Date>" +
                "        <Description>foo</Description>" +
                "    </Inner>" +
                "    <Inner>" +
                "        <Date scheduled=\"true\">2016-03-18</Date>" +
                "        <Description>bar</Description>" +
                "    </Inner>" +
                "</Outer>";

        String nestedXmlWithoutAttr =
                "<Outer>" +
                "    <Inner>" +
                "        <Date>2016-03-18</Date>" +
                "        <Description>foo</Description>" +
                "    </Inner>" +
                "    <Inner>" +
                "        <Date>2016-03-18</Date>" +
                "        <Description>bar</Description>" +
                "    </Inner>" +
                "</Outer>";

        XmlMapper mapper = new XmlMapper();
        mapper.setDefaultUseWrapper(false);
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        JaxbAnnotationModule module = new JaxbAnnotationModule();
        mapper.registerModule(module);

        Inner inner1 = mapper.readValue(xmlWithoutAttr, Inner.class);   // works
        System.out.println(inner1);

        Inner inner2 = mapper.readValue(xmlWithAttr, Inner.class);      // works
        System.out.println(inner2);

        Outer outer1 = mapper.readValue(nestedXmlWithoutAttr, Outer.class); // works
        System.out.println(outer1);

        Outer outer2 = mapper.readValue(nestedXmlWithAttr, Outer.class);    // errors
        System.out.println(outer2);
    }

}

@cowtowncoder
Copy link
Member

Thank you for reproduction. I think that cases that seemingly work are probably accidentally omitting the attribute, but the core of problem is this: unless property implied by attribute is explicitly ignored (not just by default), its existence will trigger an exception because structurally only XML elements with single textual value (element text or attribute) may be bound to String property.

Put another way: anything for which more than one piece of information is found from XML MUST be a POJO with multiple properties. Only text-only or single-attribute only (and no text content) XML content may be bound/mapped to a String.

Simplest way here, then, would be to explicitly ignore scheduled value (using, for example, @JsonIgnoreProperties({ "scheduled" }).

@boudewijnvanweert
Copy link

Is this by design? It seems like a limitation to only allow either 1 attribute or text content. I'm facing a case now where I would also need both. By the way, if I serialise it, it works perfect and shows both the attribute and the text content

@cowtowncoder
Copy link
Member

@boudewijnvanweert Is that question related to this issue? I am not sure how it relates; if not, please file a new issue.

@cowtowncoder
Copy link
Member

Test passes with 2.12.0; closing.

@cowtowncoder cowtowncoder added this to the 2.12.0 milestone Nov 14, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants