Using Jaxb To Serialize Java-Objects To Xml

Posted by {"name"=>"Palash Ray", "email"=>"paawak@gmail.com", "url"=>"https://www.linkedin.com/in/palash-ray/"} on January 11, 2015 · 11 mins read

Recently, I had a requirement of serializing some Java objects to xml. Though I had assumed it to be fairly straight forward, during implementation, I found out the hard way that it is not at all intuitive. The following are the ground rules:

  1. You need to decorate the class which you want to serialize with the @XmlRootElement annotation. This rules out having the flexibility of serializing a List or a Map directly. It has to be wrapped within a class having the @XmlRootElement annotation.

  2. Jaxb needs a default constructor. So if you have an immutable class with all fields set through the constructor, you need to provide a default constructor and set all the final fields to null.

  3. Though Jaxb works well with the List, it needs a custom adapter to serialize a Map with the @XmlJavaTypeAdapter annotation, as we will see. This is the single most pain point in using Jaxb.

The serializer code

I am using the O/X Mappers from Spring (http://docs.spring.io/spring-ws/site/reference/html/oxm.html) for marshalling. I found it very convenient, specially if I want to switch from Jaxb to say, Xstream. The code is pretty simple:

    @Override
    public void serialize(Object object, OutputStream outputStream) {
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
        marshaller.setClassesToBeBound(EmployeeGroups.class);
        Map marshallerProperties = new HashMap<>();
	  // for formatted xml
        marshallerProperties.put(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
        marshaller.setMarshallerProperties(marshallerProperties);
        Result xmlResultCapturer = new StreamResult(outputStream);
        try {
            marshaller.marshal(object, xmlResultCapturer);
        } catch (Exception e) {
            LOG.error("could not convert rmi output to xml", e);
        }
    }

This is the version using plain Jaxb:

@Override
    public void serialize(Object object, OutputStream outputStream) {
        JAXBContext jaxbContext;
        try {
            jaxbContext = JAXBContext.newInstance(EmployeeGroups.class);
        } catch (JAXBException e) {
            throw new RuntimeException(e);
        }
        Marshaller marshaller;
        try {
            marshaller = jaxbContext.createMarshaller();
        } catch (JAXBException e) {
            throw new RuntimeException(e);
        }
        // for formatted xml
        try {
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
        } catch (PropertyException e) {
            LOG.warn("Invalid property", e);
        }
        Result xmlResultCapturer = new StreamResult(outputStream);
        try {
            marshaller.marshal(object, xmlResultCapturer);
        } catch (Exception e) {
            LOG.error("could not convert rmi output to xml", e);
        }
    }

The POJO to be serialized

The POJO looks like:

@XmlRootElement
public class EmployeeGroups {
    private final int groupSize;
    private final Map> employeeGroups;
    public EmployeeGroups(int groupSize, Map> employeeGroups) {
        this.groupSize = groupSize;
        this.employeeGroups = employeeGroups;
    }
    /**
     * Default constructor, as Jaxb will not work without it
     */
    public EmployeeGroups() {
        this(0, null);
    }
    public int getGroupSize() {
        return groupSize;
    }
    public Map> getEmployeeGroups() {
        return employeeGroups;
    }
}

Attempt 1

When I try to generate xml, I get a blank document:


 

The possible cause might be that since the object is immutable without any setters for the fields, Jaxb cannot figure out the fields to serialize. So, this time we will try to explicitly ask Jaxb to serialize the fields by adding the @XmlAccessorType(XmlAccessType.FIELD) annotation to the EmployeeGroups.

Attempt 2

The POJO looks like:

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class EmployeeGroups {
...
}

The 2nd attempt gives rise to the following exception:

Caused by: com.sun.xml.internal.bind.v2.runtime.IllegalAnnotationsException: 1 counts of IllegalAnnotationExceptions
java.util.List is an interface, and JAXB can't handle interfaces.
    this problem is related to the following location:
        at java.util.List
        at private final java.util.Map com.swayam.demo.xml.EmployeeGroups.employeeGroups
        at com.swayam.demo.xml.EmployeeGroups

This message is actually misleading. What it means is that its high time for us to write our custom adapter for handling the Map.

Attempt 3

The adapter would extend the XmlAdapter. It would consume a Map> and return a concrete POJO, in our case a ListWrapper.

public class JaxbMapAdapter extends XmlAdapter, Map>> {
    @Override
    public ListWrapper marshal(Map> employeeGroups) {
        List simpleEntries = new ArrayList<>();
        for (Entry> employeeGroupEntry : employeeGroups.entrySet()) {
            SimpleMapEntry simpleMapEntry = new SimpleMapEntry();
            simpleMapEntry.setEmployeeRole(employeeGroupEntry.getKey());
            simpleMapEntry.setEmployees(employeeGroupEntry.getValue());
            simpleEntries.add(simpleMapEntry);
        }
        return new ListWrapper<>(simpleEntries);
    }
    @Override
    public Map> unmarshal(ListWrapper v) {
        throw new UnsupportedOperationException();
    }
}

The SimpleMapEntry represents an Entry in the Map, customized to our needs.

public class SimpleMapEntry {
    private EmployeeRole employeeRole;
    private List employees;
    public EmployeeRole getEmployeeRole() {
        return employeeRole;
    }
    public void setEmployeeRole(EmployeeRole employeeRole) {
        this.employeeRole = employeeRole;
    }
    public List getEmployees() {
        return employees;
    }
    public void setEmployees(List employees) {
        this.employees = employees;
    }
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((employeeRole == null) ? 0 : employeeRole.hashCode());
        result = prime * result + ((employees == null) ? 0 : employees.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        SimpleMapEntry other = (SimpleMapEntry) obj;
        if (employeeRole != other.employeeRole)
            return false;
        if (employees == null) {
            if (other.employees != null)
                return false;
        } else if (!employees.equals(other.employees))
            return false;
        return true;
    }
}

The ListWrapper is just a wrapper around a List, as Jaxb cannot handle a List, and needs a POJO to serialize.

public class ListWrapper {
    private final List list;
    public ListWrapper(List list) {
        this.list = list;
    }
    public ListWrapper() {
        this(Collections.emptyList());
    }
    public List getList() {
        return list;
    }
}

At the end of it, the test runs without error, giving the following output:



    3
    

 

Attempt 4

The adapter works fine, but since the members are immutable, we have to explicitly declare the fields to be picked up by Jaxb.

@XmlAccessorType(XmlAccessType.FIELD)
public class ListWrapper {
}

The test errors out:

ERROR [main] PlainJaxbSerializer.serialize(46) | could not convert rmi output to xml
javax.xml.bind.MarshalException
 - with linked exception:
[com.sun.istack.internal.SAXException2: class com.swayam.demo.xml.jaxb.SimpleMapEntry nor any of its super class is known to this context.
javax.xml.bind.JAXBException: class com.swayam.demo.xml.jaxb.SimpleMapEntry nor any of its super class is known to this context.]
    at com.sun.xml.internal.bind.v2.runtime.MarshallerImpl.write(MarshallerImpl.java:311)

Attempt 5

This simply means that when creating the Jaxb context, pass in the SimpleMapEntry as an argument:

JAXBContext jaxbContext;
        try {
            jaxbContext = JAXBContext.newInstance(EmployeeGroups.class, SimpleMapEntry.class);
        } catch (JAXBException e) {
            throw new RuntimeException(e);
        }

All is good, and this is the output:



    3
    
        
            SUPERVISOR
            
            
            
            
        
        
            TECHNICIAN
            
            
            
            
        
        
            MANAGER
            
            
            
            
        
    

Attempt 6

Note that the employee node is still empty. We need to explicitly set the @XmlAccessorType(XmlAccessType.FIELD) annotation:

@XmlAccessorType(XmlAccessType.FIELD)
public class Employee {
}

And everything comes as expected:



    3
    
        
            SUPERVISOR
            
                Ramu
                42
                SUPERVISOR
            
            
                Shyam
                45
                SUPERVISOR
            
            
                Radhe
                38
                SUPERVISOR
            
            
                Jesse
                44
                SUPERVISOR
            
        
        
            TECHNICIAN
            
                Sadhu
                22
                TECHNICIAN
            
            
                Yadav
                25
                TECHNICIAN
            
            
                Chris
                28
                TECHNICIAN
            
            
                Bill
                34
                TECHNICIAN
            
        
        
            MANAGER
            
                Sachin
                35
                MANAGER
            
            
                Hamid
                32
                MANAGER
            
            
                Naresh
                34
                MANAGER
            
            
                Manna
                36
                MANAGER
            
        
    

Attempt 7

The only problem as I see now is this:

            
                Manna
                36
                MANAGER
            

It should be within an . The following is the modification needed:

@XmlAccessorType(XmlAccessType.FIELD)
public class SimpleMapEntry {
    private EmployeeRole employeeRole;
    @XmlElementWrapper(name = "employees")
    @XmlElement(name = "employee")
    private List employees;

With this, we get the following output:



    3
    
        
            SUPERVISOR
            
                
                    Ramu
                    42
                    SUPERVISOR
                
                
                    Shyam
                    45
                    SUPERVISOR
                
                
                    Radhe
                    38
                    SUPERVISOR
                
                
                    Jesse
                    44
                    SUPERVISOR
                
            
        
        
            TECHNICIAN
            
                
                    Sadhu
                    22
                    TECHNICIAN
                
                
                    Yadav
                    25
                    TECHNICIAN
                
                
                    Chris
                    28
                    TECHNICIAN
                
                
                    Bill
                    34
                    TECHNICIAN
                
            
        
        
            MANAGER
            
                
                    Sachin
                    35
                    MANAGER
                
                
                    Hamid
                    32
                    MANAGER
                
                
                    Naresh
                    34
                    MANAGER
                
                
                    Manna
                    36
                    MANAGER
                
            
        
    

Sources

The sources can be found here: https://github.com/paawak/blog/tree/master/code/jaxb-demo

Reference

https://jaxb.java.net/tutorial/

http://docs.spring.io/spring-ws/site/reference/html/oxm.html