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:
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.
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.
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); MapmarshallerProperties = 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
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 Listemployees; 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
@XmlAccessorType(XmlAccessType.FIELD) public class SimpleMapEntry { private EmployeeRole employeeRole; @XmlElementWrapper(name = "employees") @XmlElement(name = "employee") private Listemployees;
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