November 15, 2012

Creating a Generic List Wrapper in JAXB

To marshal/unmarshal a list with a JAXB (JSR-222) implementation you need to create a wrapper object to hold the list. People find this onerous having to create multiple wrapper objects for this purpose.  Below I'll demonstrate that in reality you only need to create one. This post is based on an answer I gave on Stack Overflow.

Generic Wrapper

We will create a generic wrapper class with a List property annotated with @XmlAnyElement(lax=true). The type of the object used to populate this list will be based on its root element (see: Using @XmlAnyElement to Build a Generic Message).

package blog.list.wrapper;
    
import java.util.*;
import javax.xml.bind.annotation.XmlAnyElement;

public class Wrapper<T> {

    private List<T> items;

    public Wrapper() {
        items = new ArrayList<T>();
    }

    public Wrapper(List<T> items) {
        this.items = items;
    }

    @XmlAnyElement(lax=true)
    public List<T> getItems() {
        return items;
    }

}

Java Model

Below is the Java model that we'll use for this example.  With this approach we need to annotate each class that can appear in the root level collection with @XmlRootElement.

Address

In the Address class we will use the default element names that JAXB implementations derive from classes and properties.

package blog.list.wrapper;
    
import javax.xml.bind.annotation.XmlRootElement;
    
@XmlRootElement
public class Address {

    private String street;

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

}

Person

Here we will explicitly specify the element names that will be used. 

package blog.list.wrapper;
    
import javax.xml.bind.annotation.XmlRootElement;
    
@XmlRootElement(name="PERSON")
public class Person {

    private String name;

    @XmlElement(name="NAME")
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
}

XML

The following XML files will be the input to the demo code.  There are a couple of items to note about the XML:
  1. There are multiple root elements (addresses and PERSONS) that we need to unmarshal to the same class.
  2. The elements corresponding to items in the collection (address and PERSON) correspond to the @XmlRootElement annotations on Address and Person.

addresses.xml
<?xml version="1.0" encoding="UTF-8"?>
<addresses>
    <address/>
        <street>1 Some Street</street>
    </address>
    <address/>
        <street>2 Another Road</street>
    </address>
</addresses>

persons.xml
<?xml version="1.0" encoding="UTF-8"?>
<PERSONS>
    <PERSON>
        <NAME>Jane Doe</NAME>
    </PERSON>
    <PERSON>
        <NAME>John Smith</NAME>
    </PERSON>
</PERSONS>

Demo

The demo code below demonstrates how to use the Wrapper class. Since the root element can be different you will need to specify that you want to unmarshal to the wrapper class. Alternatively you could leverage the @XmlElementDecl annotation to associate multiple root elements with the wrapper class (see: JAXB and Root Elements).

package blog.list.wrapper;
    
import java.util.List;
import javax.xml.bind.*;
import javax.xml.namespace.QName;
import javax.xml.transform.stream.StreamSource;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(Wrapper.class, Person.class,
                Address.class);

        // Unmarshal
        Unmarshaller unmarshaller = jc.createUnmarshaller();
        List<Address> addresses = unmarshal(unmarshaller, Address.class, "addresses.xml");
        List<Person> persons = unmarshal(unmarshaller, Person.class, "persons.xml");
        
        // Marshal
        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshal(marshaller, addresses, "addresses");
        marshal(marshaller, persons, "PERSONS");
     }

    /**
     * Unmarshal XML to Wrapper and return List value.
     */
    private static <T> List<T> unmarshal(Unmarshaller unmarshaller,
            Class<T> clazz, String xmlLocation) throws JAXBException {
        StreamSource xml = new StreamSource(xmlLocation);
        Wrapper<T> wrapper = (Wrapper<T>) unmarshaller.unmarshal(xml,
                Wrapper.class).getValue();
        return wrapper.getItems();
    }

    /**
     * Wrap List in Wrapper, then leverage JAXBElement to supply root element 
     * information.
     */
    private static void marshal(Marshaller marshaller, List<?> list, String name)
            throws JAXBException {
        QName qName = new QName(name);
        Wrapper wrapper = new Wrapper(list);
        JAXBElement<Wrapper> jaxbElement = new JAXBElement<Wrapper>(qName,
                Wrapper.class, wrapper);
        marshaller.marshal(jaxbElement, System.out);
    }

}

Output

Below is the output from running the demo code. 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addresses>
    <address/>
        <street>1 Some Street</street>
    </address>
    <address/>
        <street>2 Another Road</street>
    </address>
</addresses>
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<PERSONS>
    <PERSON>
        <NAME>Jane Doe</NAME>
    </PERSON>
    <PERSON>
        <NAME>John Smith</NAME>
    </PERSON>
</PERSONS>

Further Reading 

If you enjoyed this post then you may also be interested in the following:


4 comments:

  1. Blaise,

    This looks interesting - is there any way to signal to the XML Schema generator what the plural name should be? For example if I have a JAX-RS method that returns:

    @GET
    public Wrapper get() {};

    This will fail to generate the matching XML Schema definition in the WADL because Wrapper isn't a @XmlRootElement. Is there any obvious mechanism which would statically allow us to specify the plural name / namespace without access to the Marshall/Unmarshall mechanism.

    I guess a set of trivial sub classes would solve this problem; but I was interested to hear if you had another solution that wasn't obvious.

    Thanks,

    Gerard

    ReplyDelete
    Replies
    1. Hi Gerard,

      You could have multiple @XmlElementDecl annotations that correspond to the Wrapper class on an ObjectFactory (a class annotated with @XmlRegistry).

      See:
      - JAXB and Root Elements

      -Blaise

      Delete
    2. except it doesn't work with enums. so dissappointed with moxy's enum handling. it is just terrible. i switched to moxy for json, now it broke all the classes having list of enums

      Delete
    3. Can you enter a bug for the issue you are seeing with enums? Also if you are able to attach a small example that demonstrates the problem that would be a big help:
      - https://bugs.eclipse.org/bugs/enter_bug.cgi?product=EclipseLink

      Delete