JAXB and xsd:choice

A few days ago, when using JAXB in my final project, I came across an unpleasant mapping of a choice element in the schema. Actually the mapping was correct; the problem was in the schema and could be easily overcame. Lets start be analyzing the default JAXB behavior when processing the following schema (tested with Reference Implementation, vJAXB 2.1.10 in JDK 6):

<xsd:element name="A">
  <xsd:complexType>
    <xsd:simpleContent>
      <xsd:extension base="xsd:string">
        <xsd:attribute name="AttrOfA" type="xsd:string"/>
      </xsd:extension>
    </xsd:simpleContent>
  </xsd:complexType>
</xsd:element>

<xsd:element name="B">
  <xsd:complexType>
    <xsd:simpleContent>
      <xsd:extension base="xsd:string">
        <xsd:attribute name="AttrOfB" type="xsd:int"/>
      </xsd:extension>
    </xsd:simpleContent>
  </xsd:complexType>
</xsd:element>

<!-- (1) -->
<xsd:complexType name="SimpleChoice">
  <xsd:choice>
    <xsd:element ref="A"/>
    <xsd:element ref="B"/>
  </xsd:choice>
</xsd:complexType>

<!-- (2) -->
<xsd:complexType name="SimpleNElemsChoice">
  <xsd:choice>
    <xsd:element ref="A" maxOccurs="unbounded"/>
    <xsd:element ref="B" maxOccurs="unbounded"/>
  </xsd:choice>
</xsd:complexType>

<!-- (3) -->
<xsd:complexType name="UnboundedChoice">
  <xsd:choice maxOccurs="unbounded">
    <xsd:element ref="A"/>
    <xsd:element ref="B"/>
  </xsd:choice>
</xsd:complexType>

In the first two choices the mappings are straightforward: in (1) a class with two properties is generated (one for A and the other for B); (2) is similar but the properties are lists of A and B, respectively. However, JAXB doesn’t actually check for the correctness of the objects before marshalling. This means that the objects resulting from the following code can be marshaled, and will result in XML that doesn’t comply with the schema.

SimpleChoice sc = new SimpleChoice();
sc.setA(a);
sc.setB(b);

SimpleNElemsChoice snec = new SimpleNElemsChoice();
snec.getA().add(a);
snec.getA().add(a);
snec.getB().add(b);

This is not much of a problem since you’re controlling the object construction. However, it gets worse when unmarshaling, because the choice’s structure in not enforced. This means that the following XML content is successfully unmarshaled:

<SimpleChoice>
  <A AttrOfA="AttrOfA">AValue</A>
  <B AttrOfB="1">BValue</B>
</SimpleChoice>

<SimpleNElemsChoice>
  <A AttrOfA="AttrOfA">AValue</A>
  <B AttrOfB="1">BValue</B>
  <B AttrOfB="1">BValue</B>
  <B AttrOfB="1">BValue</B>
  <A AttrOfA="AttrOfA">AValue</A>
</SimpleNElemsChoice>

Note that in the second choice both the elements and their order are not complying to the schema. JAXB won’t check this! There are two solutions: pre-validate the XML document against the corresponding schema or use the getters of the different properties to validate the structure in your code after unmarshaling.

That said, lets go back to number (3). That kind of structure allows multiple A and B elements in any order. To reflect this freedom, JAXB generates a class with a single list that accepts A and B instances.

public class UnboundedChoice {

    @XmlElements({
        @XmlElement(name = "B", type = B.class),
        @XmlElement(name = "A", type = A.class)
    })
    protected List<Object> aOrB;

This isn’t so bad when marshaling, because you just need to add the objects to the list. But when unmarshaling, you’ll have to check the type of each object in the list and process it accordingly. Boring! Specially if you have A, B, C, D, E, F and you need separate lists for each type. This may not be a problem in some scenarios, but in mine it really didn’t fit.

A possible workaround is to do a little adjustment to the schema just to generate the JAXB classes: turn the unbounded choice into a sequence of unbounded elements:

<xsd:complexType name="UnboundedChoice">
    <xsd:sequence>
      <xsd:element ref="A" minOccurs="0" maxOccurs="unbounded"/>
      <xsd:element ref="B" minOccurs="0" maxOccurs="unbounded"/>
    </xsd:sequence>
</xsd:complexType>

The difference here is that the A’s have to appear before the B’s. But this isn’t really a problem since JAXB doesn’t check the ordering of the XML data! In the marshaling process the elements will actually be put in order, but that’s OK because it still complies to the unbounded choice’s schema (3). On the other hand, in the unmarshaling process the elements can appear in any order because JAXB won’t check it accordingly to the adjusted schema. So, you can have the XML with unbounded choice:

<UnboundedChoice>
  <A AttrOfA="AttrOfA">AValue</A>
  <B AttrOfB="1">BValue</B>
  <B AttrOfB="1">BValue</B>
  <A AttrOfA="AttrOfA">AValue</A>
  <B AttrOfB="1">BValue</B>
</UnboundedChoice>

But you’ll actually have two separated lists in the generated class 🙂

public class UnboundedChoice {

    @XmlElement(name = "A")
    protected List<A> a;
    @XmlElement(name = "B")
    protected List<B> b;

Remember that the original schema should still be used for validating XML documents.

The adjustment I made actually had another advantage in my scenario. Suppose you want a single A element and multiple B’s in any order. With unbounded choice you can’t specify this because if you set maxOccurs in an element, the unbounded limit on the choice will allow that element to be present again. With the sequence you can actually do that:

<xsd:complexType name="UnboundedChoice">
    <xsd:sequence>
      <xsd:element ref="A" minOccurs="0" maxOccurs="1"/>
      <xsd:element ref="B" minOccurs="0" maxOccurs="unbounded"/>
    </xsd:sequence>
</xsd:complexType>

And you’ll get the follow class:

public class UnboundedChoice {

    @XmlElement(name = "A")
    protected A a;
    @XmlElement(name = "B")
    protected List<B> b;

JAXB will actually check the presence of a single A element. Again, since JAXB doesn’t check the order, the A element can be anywhere in the “choice”.

To sum up: we’re using classes generated from an adjusted schema that is compliant with the original. Note that the opposite isn’t true, but since the ordering won’t be checked, JAXB will return the elements appropriately anyway.

Hope it helps! If anyone has a better solution, please feel free to comment! Also, you can download the source code of the demo.