October 21, 2010

JAXB and XSD Choice: @XmlElements

XML Schema has the concept of a choice element.  The choice element indicates that one of the elements defined within the choice may occur at this point in the XML document.  In this example I will demonstrate how easily this can be mapped using JAXB.


XML Schema

We will map our object model to the following XML schema that contains a choice structure.  In this XML schema the choice structure means that after the name element, one of the following elements may occur:  address, phone-number, or note.

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <xsd:element name="customer" type="customer"/>
    <xsd:complexType name="customer">
        <xsd:sequence>
            <xsd:element name="name" type="xsd:string" minOccurs="0"/>
            <xsd:choice>
                <xsd:element name="address" type="address"/>
                <xsd:element name="phone-number" type="phoneNumber"/>
                <xsd:element name="note" type="xsd:string"/>
            </xsd:choice>
        </xsd:sequence>
    </xsd:complexType>
    <xsd:complexType name="address">
        <xsd:sequence>
            <xsd:element name="city" type="xsd:string" minOccurs="0"/>
            <xsd:element name="street" type="xsd:string" minOccurs="0"/>
        </xsd:sequence>
    </xsd:complexType>
    <xsd:complexType name="phoneNumber">
        <xsd:simpleContent>
            <xsd:extension base="xsd:string">
                <xsd:attribute name="type" type="xsd:string"/>
            </xsd:extension>
        </xsd:simpleContent>
    </xsd:complexType>
</xsd:schema>

Java Model

We will use the following domain model for this example.  The model represents information about a customer.

Customer

Our Customer object has a property called "contactInfo" of type Object that is intended to hold some sort of information related to contacting that customer.  In this example that data can be in the form of a domain object (Address or PhoneNumber) or just a String.

We will use the @XmlElements annotation to map to the choice structure.  Inside @XmlElements are multiple @XmlElement annotations that link a element with a given name and namespace to a class.  During a marshal operation the type of object held by the "contactInfo" property will dictate the element produced, and during an unmarshal operation the element information will dictate what type of object is instantiated.

package blog.choice;

import javax.xml.bind.annotation.*;

@XmlAccessorType(XmlAccessType.FIELD)
public class Customer {

    private String name;

    @XmlElements(value = { 
            @XmlElement(name="address", 
                        type=Address.class),
            @XmlElement(name="phone-number", 
                        type=PhoneNumber.class),
            @XmlElement(name="note", 
                        type=String.class)
    })
    private Object contactInfo;

}

Address

package blog.choice;

public class Address {

    private String street;

    private String city;

}

PhoneNumber

package blog.choice;

import javax.xml.bind.annotation.*;

@XmlAccessorType(XmlAccessType.FIELD)
public class PhoneNumber {

    @XmlAttribute 
    private String type;

    @XmlValue 
    private String number;

}

Demo Code

Customer with Address

First we will set an instance of Address on the "contactInfo" property:

package blog.choice;

import javax.xml.bind.*;

public class DemoWithAddress {

    public static void main(String[] args) throws Exception {
        Customer customer = new Customer();
        customer.setName("Jane Doe");

        Address address = new Address();
        address.setStreet("1 A Street");
        address.setCity("Any Town");
        customer.setContactInfo(address);

        JAXBContext jc = JAXBContext.newInstance(Customer.class, PhoneNumber.class, Address.class);
        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(customer, System.out);
    }

}

This will result in the following XML:

<customer>
   <name>Jane Doe</name>
   <address>
      <city>Any Town</city>
      <street>1 A Street</street>
   </address>
</customer>

Customer with PhoneNumber

Next we will set an instance of PhoneNumber on the "contactInfo" property:

package blog.choice;

import javax.xml.bind.*;

public class DemoWithPhoneNumber {

    public static void main(String[] args) throws Exception {
        Customer customer = new Customer();
        customer.setName("Jane Doe");

        PhoneNumber phoneNumber = new PhoneNumber();
        phoneNumber.setType("work");
        phoneNumber.setNumber("555-1111");
        customer.setContactInfo(phoneNumber);

        JAXBContext jc = JAXBContext.newInstance(Customer.class, PhoneNumber.class, Address.class);
        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(customer, System.out);
    }

}

This will result in the following XML:

<customer>
   <name>Jane Doe</name>
   <phone-number type="work">555-1111</phone-number>
</customer>

Customer with String

Finally we will set an instance of String on the "contactInfo" property:


package blog.choice;

import javax.xml.bind.*;

public class DemoWithString {

    public static void main(String[] args) throws Exception {
        Customer customer = new Customer();
        customer.setName("Jane Doe");

        customer.setContactInfo("jane.doe@example.com");

        JAXBContext jc = JAXBContext.newInstance(Customer.class, PhoneNumber.class, Address.class);
        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(customer, System.out);
    }

}

This will result in the following XML:

<customer>
   <name>Jane Doe</name>
   <note>jane.doe@example.com</note>
</customer> 

Further Reading

If you enjoyed this post you may also be interested in:
 

7 comments:

  1. What about the case where two of the choices are of the same type, say phoneNumber is String and faxNumber is String as well.

    ReplyDelete
  2. If we add @XmlElement(name="fax-number", type=PhoneNumber.class) to the @XmlElements annotation in our example the following will happen.

    1. The unmarshal will work as expected, both the "phone-number" and "fax-number" elements will cause a PhoneNumber object to be instanitated.

    2. The marshal operation will choose one of the element names to use.

    In my opinion you should be able to set your property type to JAXBElement, and use that to preserve the element name. This does not currently work in Metro or MOXy. I have entered the following bug to track this issue:

    - https://bugs.eclipse.org/328646

    ReplyDelete
  3. Question about "reverse-engineering" the request where the consumer/client is working off my WSDL with the option. How do you know which one of the choices was used or if they even entered one of the choice values which are "required"?

    ReplyDelete
  4. For "reverse-engineering" you will need to infer which choice was supplied by the resulting value on the model object. To enforce that they supplied a required choice you can set a schema on the unmarshaller that will force it to validate.

    ReplyDelete
  5. Hi,

    I generated the JAXB classes using the above xsd in RAD7.

    but the Customer class is not same as the above..
    looks choice is not woking please find the classes below.

    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlType(name = "customer", propOrder = {
    "name",
    "address",
    "phoneNumber",
    "note"
    })
    public class Customer {

    protected String name;
    protected Address address;
    @XmlElement(name = "phone-number")
    protected PhoneNumber phoneNumber;
    protected String note;

    Do you think the problem in RAD ?

    ReplyDelete
  6. Hi Madhava,

    The behaviour you are seeing is correct. I have written a new post to better answer your question:

    - XML Schema to Java - XSD Choice

    -Blaise

    ReplyDelete
  7. Thanks! This was excellent and clear information. I had been googling the web to figure out how to generate the xsd choice but it was hard to find since the word "choice" also seems to be overloaded with non-XML-related semantics ;-)

    Anyway, I've bookmarked you blog as a valuable resource!

    ReplyDelete