Skip to content
This repository has been archived by the owner on Jan 22, 2019. It is now read-only.

Jackson interpretation of XMLElements not compatible with JAXB #51

Closed
rpatrick00 opened this issue Nov 16, 2015 · 19 comments
Closed

Jackson interpretation of XMLElements not compatible with JAXB #51

rpatrick00 opened this issue Nov 16, 2015 · 19 comments

Comments

@rpatrick00
Copy link

I have a very simple example of a class with a List member that uses polymorphism. The Java code looks like this:

@XmlRootElement(name = "company")
@XmlAccessorType(XmlAccessType.FIELD)
public class Company {

  @XmlElements({
      @XmlElement(type = DesktopComputer.class, name = "desktop"),
      @XmlElement(type = LaptopComputer.class, name = "laptop")
  })
  private List computers;
  ...
}

JAXB serializes the output correctly (or at least in the way that I want it) like this:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<company>
    <computers>
        <desktop id="computer-1">
            <location>Bangkok</location>
        </desktop>
        <desktop id="computer-2">
            <location>Pattaya</location>
        </desktop>
        <laptop id="computer-3">
            <vendor>Apple</vendor>
        </laptop>
    </computers>
</company>

Jackson JAXB annotations wraps each resulting element in the list in a redundant <computers> tag like this:

<?xml version='1.1' encoding='UTF-8'?><company><computers>
    <computers>
      <desktop id="computer-1">
        <location>Bangkok</location>
      </desktop>
    </computers>
    <computers>
      <desktop id="computer-2">
        <location>Pattaya</location>
      </desktop>
    </computers>
    <computers>
      <laptop id="computer-3">
        <vendor>Apple</vendor>
      </laptop>
    </computers>
  </computers>
</company>

How can I eliminate the extra wrapping Jackson is doing to generate the same shape as JAXB?

@cowtowncoder
Copy link
Member

This is the case of differing defaults; Jackson defaults to using wrapper element, whereas JAXB does not. You can use Jackson annotation from module jackson-dataformat-xml (I forget the name, something like @JacksonXmlWrapperElement) on specified property, and XML module also has default configuration setting (for XmlMapper) that may be changed to choose JAXB default setting.
Let me know if you can't locate the settings; I forget the exact methods.

@rpatrick00
Copy link
Author

@JacksonXmlElement(useWrapping=false) does exactly the opposite of what I want/need. It removes the outer list wrapper but the individual list elements are still being wrapped.

I am not seeing anything that allows me to change the defaults to JAXB, sorry.

@cowtowncoder
Copy link
Member

Ok. So this is actually more related to polymorphic type handling, where type identifier is to be used as the element wrapper. Would it be possible to include definitions of value types (or at least one)?
I also noticed that type declaration (private List computers;) is missing generic type (I assume it should be something like List<Computer> computers?)

@rpatrick00
Copy link
Author

Sorry, I was bitten by the web rendering of the unescaped <Computer> tag because my code does have the List type information:

@XmlRootElement(name="company")
@XmlAccessorType(XmlAccessType.FIELD)
public class Company {
  //@XmlElementWrapper(name = "computers")
  @XmlElements({
      @XmlElement(type = DesktopComputer.class, name = "desktop"),
      @XmlElement(type = LaptopComputer.class, name = "laptop")
  })
  private List<Computer> computers;
...

I am not sure exactly what you mean by including the "definitions of value types" (isn't that what the @XmlElements annotation above is doing?). My Computer super class also has the @XmlSeeAlso annotation with the type information:

@XmlSeeAlso({ LaptopComputer.class, DesktopComputer.class })
@XmlAccessorType(XmlAccessType.FIELD)
public class Computer {
  @XmlAttribute
  @XmlID
  private String id;
  public String getId() {
    return id;
  }
  public void setId(String id) {
    this.id = id;
  }
}

I have full control over the source so I can add additional annotations, if required, to get the proper XML shape.

Thanks,
Robert

@cowtowncoder
Copy link
Member

I guess it would be great to split this problem into two parts, due to 2 possible problem areas:

  1. Handling of JAXB annotations (instead of Jackson native ones)
  2. Serialization as XML

So, a reproduction that only did one of the two would be most helpful in figuring out the actual issue.
I would probably start with JAXB + JSON use case; and if that does not fail, try Jackson annotations with XML output to try to reproduce the problem.

@rpatrick00
Copy link
Author

I will work on this and at a minimum, provide more details and a reproducer of the specific problem(s).

@rpatrick00
Copy link
Author

JAXB annotations with JSON fails in Jackson 2.7.0-rc2:

Using Jackson to read the file previously written by Jackson:
[WARNING]
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.
java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAcces
sorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:293)
at java.lang.Thread.run(Thread.java:745)
Caused by: com.fasterxml.jackson.databind.JsonMappingException: Unexpected token
(FIELD_NAME), expected START_OBJECT: need JSON Object to contain As.WRAPPER_OBJ
ECT type information for class test.Computer
at [Source: company-jackson.json; line: 3, column: 5](through reference chain:
test.Company["computers"]->java.util.ArrayList[0])
at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingE
xception.java:216)
at com.fasterxml.jackson.databind.DeserializationContext.wrongTokenExcep
tion(DeserializationContext.java:962)
at com.fasterxml.jackson.databind.jsontype.impl.AsWrapperTypeDeserialize
r._deserialize(AsWrapperTypeDeserializer.java:91)
at com.fasterxml.jackson.databind.jsontype.impl.AsWrapperTypeDeserialize
r.deserializeTypedFromObject(AsWrapperTypeDeserializer.java:49)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserialize
WithType(BeanDeserializerBase.java:992)
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deser
ialize(CollectionDeserializer.java:279)
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deser
ialize(CollectionDeserializer.java:249)
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deser
ialize(CollectionDeserializer.java:26)
at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize
(SettableBeanProperty.java:490)
at com.fasterxml.jackson.databind.deser.impl.FieldProperty.deserializeAn
dSet(FieldProperty.java:101)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserial
ize(BeanDeserializer.java:257)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(Bea
nDeserializer.java:125)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMa
pper.java:3773)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.ja
va:2657)
at test.RunThis.readJacksonFile(RunThis.java:61)
at test.RunThis.main(RunThis.java:94)
... 6 more
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.236 s
[INFO] Finished at: 2015-12-16T06:51:35-06:00
[INFO] Final Memory: 21M/378M
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.codehaus.mojo:exec-maven-plugin:1.4.0:java (r
un-this) on project jackson-list-test: An exception occured while executing the
Java class. null: InvocationTargetException: Unexpected token (FIELD_NAME), expe
cted START_OBJECT: need JSON Object to contain As.WRAPPER_OBJECT type informatio
n for class test.Computer
[ERROR] at [Source: company-jackson.json; line: 3, column: 5](through reference
chain: test.Company["computers"]->java.util.ArrayList[0])
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e swit
ch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please rea
d the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionE
xception

@rpatrick00
Copy link
Author

However, the JSON generated does not have the extra layer of list element wrappers (i.e., it is what I would expect). Full reproducer is at https://github.com/rpatrick00/jackson-list-test

{
  "computers" : [ {
    "desktop" : {
      "id" : "computer-1",
      "location" : "Bangkok"
    }
  }, {
    "desktop" : {
      "id" : "computer-2",
      "location" : "Pattaya"
    }
  }, {
    "laptop" : {
      "id" : "computer-3",
      "vendor" : "Apple"
    }
  } ]
}

@rpatrick00
Copy link
Author

In switching to Jackson annotations (so that I could switch to the XML serialization part of the problem), I am able to generate the proper JSON but Jackson is still failing to read it back with Jackson 2.7.0-rc2. Maybe I omitted something in the conversion from JAXB annotations (since I am less familiar with them than JAXB annotations)? Here are the details (reproducer is the same as above, just changed the POJO annotations as shown below). The reproducer with Jackson annotations is available at https://github.com/rpatrick00/jackson-xml-list-test:.

Error:

Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Unexpected token (FIELD_NAME), expected START_OBJECT: need JSON Object to contain As.WRAPPER_OBJECT type information for class test.Computer
 at [Source: company-jackson.json; line: 3, column: 5] (through reference chain: test.Company["computers"]->java.util.ArrayList[0])
    at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:216)
    at com.fasterxml.jackson.databind.DeserializationContext.wrongTokenException(DeserializationContext.java:962)
    at com.fasterxml.jackson.databind.jsontype.impl.AsWrapperTypeDeserializer._deserialize(AsWrapperTypeDeserializer.java:91)
    at com.fasterxml.jackson.databind.jsontype.impl.AsWrapperTypeDeserializer.deserializeTypedFromObject(AsWrapperTypeDeserializer.java:49)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:992)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:279)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:249)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:26)
    at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:490)
    at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:95)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:257)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:125)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3773)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2657)
    at test.RunThis.readJacksonFile(RunThis.java:61)
    at test.RunThis.main(RunThis.java:94)
    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:497)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

Company.java

@JsonRootName(value = "company")
public class Company {
    private List<Computer> computers;
    public Company() {
        computers = new ArrayList<Computer>();
    }
    public List<Computer> getComputers() {
        return computers;
    }
    public void setComputers(List<Computer> computers) {
        this.computers = computers;
    }
    public Company addComputer(Computer computer) {
        if (computers == null) {
            computers = new ArrayList<Computer>();
        }
        computers.add(computer);
        return this;
    }
}

Computer.java

@JsonIdentityInfo(
        generator = ObjectIdGenerators.PropertyGenerator.class,
        property = "id"
)
@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.WRAPPER_OBJECT,
        property = "type"
)
@JsonSubTypes({
        @JsonSubTypes.Type(value = DesktopComputer.class, name = "desktop"),
        @JsonSubTypes.Type(value = LaptopComputer.class, name = "laptop")
})
public class Computer {
    private String id;
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
}

DesktopComputer.java

@JsonTypeName("desktop")
public class DesktopComputer extends Computer {
    @JsonProperty("location")
    private String location;
    public String getLocation() {
        return location;
    }
    public void setLocation(String location) {
        this.location = location;
    }
}

LaptopComputer.java

@JsonTypeName("laptop")
public class LaptopComputer extends Computer {
    @JsonProperty("vendor")
    private String vendor;
    public String getVendor() {
        return vendor;
    }
    public void setVendor(String vendor) {
        this.vendor = vendor;
    }
}

JSON file generated

{
  "computers" : [ {
    "desktop" : {
      "id" : "computer-1",
      "location" : "Bangkok"
    }
  }, {
    "desktop" : {
      "id" : "computer-2",
      "location" : "Pattaya"
    }
  }, {
    "laptop" : {
      "id" : "computer-3",
      "vendor" : "Apple"
    }
  } ]
}

@rpatrick00
Copy link
Author

Switching the native Jackson annotations to serialize XML instead of JSON does two undesirable things:

1.) It throws the same error as above
2.) It generates the XML with the unnecessary and unwanted "list element" wrappers

<?xml version='1.1' encoding='UTF-8'?>
<company>
  <computers>
    <computers>
      <desktop>
        <id>computer-1</id>
        <location>Bangkok</location>
      </desktop>
    </computers>
    <computers>
      <desktop>
        <id>computer-2</id>
        <location>Pattaya</location>
      </desktop>
    </computers>
    <computers>
      <laptop>
        <id>computer-3</id>
        <vendor>Apple</vendor>
      </laptop>
    </computers>
  </computers>
</company>

The reproducer with Jackson annotations is available at https://github.com/rpatrick00/jackson-xml-list-test. If you run the RunThis main class without any args, it will serialize XML. Add the -useJson arg to get it to serialize Json instead.

@rpatrick00
Copy link
Author

In Jackson 2.4.3, we were able to strip out these unwanted wrapper objects using a custom serializer/deserializer (even though we would have preferred not to have to write custom code, it was our only option).

That custom code no longer works in Jackson 2.6.3 and newer (didn't test the intermediate versions to see where it broke) and, of course, requires the use of Jackson annotations to register them. If I remember correctly, the error when we tried to use the custom serializers/deserializers to strip out the wrappers in 2.6.3 was very similar to, if not exactly, the error above.

In my opinion, it would be very desirable to eliminate the need for the custom serializers/deserializers to make it possible to get Jackson's XML output with JAXB annotations to match JAXB exactly. While it would be nice for us if this were the default when using the Jaxb Annotations Module, I would be ok with having to set additional mapper/module properties, if necessary, but would like to not have to add Jackson annotations.

@cowtowncoder
Copy link
Member

@rpatrick00 Thank you for the test and investigation! I will need to read this with thought, hoping to start resolving parts of the problem.

@cowtowncoder
Copy link
Member

@rpatrick00 Interesting. I can reproduce the basic jackson-databind problem with the example. Suspecting it is related to combination of Object and Type Id somehow, will dig deeper.

@cowtowncoder
Copy link
Member

Ok, yes. And another piece of the puzzle is that handling that is needed to allow JSON Object wrapped Object Id (for JSOG) is somehow causing the issue, as an unintended side effect. Change probably went in 2.5 (guessing it might be issue FasterXML/jackson-databind#669) -- and building from tag 2.5.0 does indeed pass, and latest 2.5 from branch fails. So that's what triggers the problem.

@cowtowncoder
Copy link
Member

Ok, first things first: the underlying problem with databind is fixed (for 2.6.5 patch, and master for 2.7.0).
This was an uncaught regression caused by an improvement to allow JSOG-style object ids (which are simple JSON Object wrappers... something not originally thought to be needed, or useful, plus tricky to deal in streaming approach). I am glad this was caught so big thank you for the report and detective work.

Now, as to XML/JAXB part, it appears like JAXB annotations are not to blame, as the results are the same with equivalent Jackson @JsonTypeInfo annotations. Polymorphic type handling is tricky on XML side, and double so with Lists without wrapper element.

I will try to work on this for XML module next.

@cowtowncoder
Copy link
Member

Will close this issue now, assuming problem exists within XML module, and is not due to translation of JAXB annotations itself.

@rpatrick00
Copy link
Author

So is there an existing issue on the XML side?

@cowtowncoder
Copy link
Member

@rpatrick00 I assumed one of existing issues would cover it. But if not, a new one would be needed over there. If I understand issue correctly it is combination of polymorphic serialization as XML, using As.WRAPPER_OBJECT producing output that has one more element than what you would want.

Description few comments up should cover it, although due to a fix to master of jackson-databind, only part of it fails.

I'll file a new issue just in case, linking back to this one -- if there is a duplicate, it can be closed, but it's better to have two than none.

@cowtowncoder
Copy link
Member

Re-created remaining problem as: FasterXML/jackson-dataformat-xml#178

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants