JAXB parent-child cycle problem
I wrote a RESTful web service that was the backend for an iOS app. Since I wrote the the web administration app using Spring @MVC 3, I used that for the web service also instead of Jersey. One cool thing is how transparent the marshaling of your objects is. Here is a simple example.
1 2 3 4 5 |
@RequestMapping("/{familyName}/{givenName}") public @ResponseBody Composer findComposerByFullName(@PathVariable String familyName, @PathVariable String givenName) { return composerService.findComposerByFullName(familyName,givenName)); } |
Notice what you don’t see. (How’s that for a sentence? :)) The web service simply returns a Composer object (obtained from the composerService) – you don’t see any gyrations for returning XML or JSON! If the client requests XML, then Spring will automatically use JAXB to convert your Java object into XML. If Jackson is on your classpath and the client requests JSON then Spring will automatically marshal your object into JSON. Neat. (You can use other marshaling technologies. See the Resources below.)
Unless you’re returning an object graph that includes a bidirectional parent/child relationship.
Here is an example parent which is also a JPA entity. One composer has many works, and a work belongs to one composer. It is not necessary that it be an Entity for the marshaling to work; it’s just that JPA bidirectional associations are commonly used along with the topic at hand.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@Entity @XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) public class Composer implements Serializable, Comparable<Composer> { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Basic(optional = true) @XmlElement(name = "givenName", required = false) @Column(name = "givenName", length = 128) private String givenName; @Basic(optional = true) @XmlElement(name = "middleName", required = false) @Column(name = "middleName", length = 128) private String middleName; @Basic(optional = false) @XmlElement(name = "familyName", required = true) @Column(name = "familyName", length = 255) private String familyName; @OneToMany(cascade = CascadeType.ALL, mappedBy = "composer", targetEntity = Work.class, fetch = FetchType.EAGER) @XmlElementWrapper(name = "works") @XmlElement(name = "work", required = true) private Set<Work> works = new HashSet<Work>(); |
And here is an example child. Since the relationship is bidirectional, there is a reference to the composer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
@Entity @XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) public class Work implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Basic(optional = false) @Column(name = "id", nullable = false) private Long id; @Basic(optional = false) @Column(name = "title") private String title; @Basic(optional = true) @Temporal(TemporalType.DATE) private Date completionDate; @Basic(optional = true) @Temporal(TemporalType.DATE) private Date premiereDate; @Basic(optional = true) @Column(name = "instrumentation") private String instrumentation; @ManyToOne(fetch = FetchType.EAGER, optional = false) @JoinColumn(name = "COMPOSER_ID") private Composer composer; |
Let’s set up a simple object graph with a Composer who has one Work. Then marshal the composer to
stdout with a small helper class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public class JAXBhelper { private Logger logger = LoggerFactory.getLogger(this.getClass()); public static void main(String[] args) { Composer composer = new Composer("Ludwig", "Beethoven"); composer.setId(123L); composer.setOtherName("van"); composer.setBirthDate(new Date()); composer.setDeathDate(new Date()); Work work = new Work(); work.setPremiereDate(new Date()); work.setCompletionDate(new Date()); work.setInstrumentation("kazoo, comb, wax paper"); work.setTitle("Cacophony No. 2.718"); composer.add(work); JAXBhelper helper = new JAXBhelper(); helper.xmlToStdout(composer); } public void xmlToStdout(Composer composer) { try { JAXBContext jaxbc = JAXBContext.newInstance(Composer.class); Marshaller marshaller = jaxbc.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); marshaller.marshal(composer, System.out); } catch (JAXBException e) { e.printStackTrace(); this.logger.error(e.getMessage()); } } } |
Oops. It looks like JAXB doesn’t like this. This what the program barfs:
1 2 3 4 5 6 7 8 9 |
javax.xml.bind.MarshalException - with linked exception: [com.sun.istack.SAXException2: A cycle is detected in the object graph. This will cause infinitely deep XML: Composer id:'123' ln:'Beethoven' gn:'Ludwig' mn:'null' Cacophony No. 2.718 -> com.rockhoppertech.marshal.domain.Work@e78ffa9d id=null title='Cacophony No. 2.718' cid=123 -> Composer id:'123' ln:'Beethoven' gn:'Ludwig' mn:'null' Cacophony No. 2.718 ] at com.sun.xml.bind.v2.runtime.MarshallerImpl.write(MarshallerImpl.java:318) etc. |
The XML solution
To fix the XML the solution is fairly simple. Just mark the parent as @XmlTransient.
1 2 3 4 5 |
@ManyToOne(fetch = FetchType.EAGER, optional = false) @JoinColumn(name = "COMPOSER_ID") // prevent a cycle @XmlTransient private Composer composer; |
You now get this output.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <composer> <id>123</id> <givenName>Ludwig</givenName> <familyName>Beethoven</familyName> <works> <work> <title>Cacophony No. 2.718</title> <completionDate>2011-10-26T17:28:04.994-04:00</completionDate> <premiereDate>2011-10-26T17:28:04.994-04:00</premiereDate> <instrumentation>kazoo, comb, wax paper</instrumentation> </work> </works> </composer> |
The Java solution
What about your Java object graph when this XML is deserialized? There is no attribute in the work element for the composer. But, you see that the work is a child element of composer so that information is not lost. Let’s check what happens with a few more utility methods.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
public void saveXML(Composer composer, String fileName) { try { JAXBContext jaxbc = JAXBContext.newInstance(Composer.class); Marshaller marshaller = jaxbc.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); OutputStream out = null; try { out = new FileOutputStream(fileName); marshaller.marshal(composer, out); } catch (FileNotFoundException e) { e.printStackTrace(); } finally { try { out.close(); } catch (IOException e) { } } } catch (JAXBException e) { e.printStackTrace(); this.logger.error(e.getMessage()); } } public Composer readComposerXML(String fileName) { Composer composer = null; try { JAXBContext jaxbc = JAXBContext.newInstance(Composer.class); Unmarshaller marshaller = jaxbc.createUnmarshaller(); InputStream file = null; try { file = new FileInputStream(fileName); composer = (Composer) marshaller.unmarshal(file); } catch (FileNotFoundException e) { e.printStackTrace(); this.logger.error(e.getMessage()); } finally { try { file.close(); } catch (IOException e) { } } } catch (JAXBException e) { e.printStackTrace(); this.logger.error(e.getMessage()); } return composer; } |
Now in the main method let’s test the marshal/unmarshal process. We save the composer to a file and then read it back. Then we look at each work and print out the composer info.
1 2 3 4 5 6 7 |
... helper.saveXML(composer, "composer.xml"); Composer read = helper.readComposerXML("composer.xml"); for(Work w: read.getWorks()) { System.out.println("Title: " + w.getTitle()); System.out.println("Composer: " + w.getComposer()); } |
Here is the output.
1 2 |
Title: Cacophony No. 2.718 Composer: null |
Since the composer field in Work has been annotated as being XmlTransient, it is not restored on unmarshal. We have to do that ourselves. Luckily it is simple. Just implement this method in the child class (Work) to restore the reference to the parent.
1 2 3 |
public void afterUnmarshal(Unmarshaller u, Object parent) { this.composer = (Composer) parent; } |
Eye candy
You can specify the ordering of the elements output by JAXB with the XmlType annotation.
1 2 3 4 |
@XmlType(name = "ComposerType", propOrder = { "id", "familyName", "givenName", "middleName", "works" }) public class Composer implements Serializable, Comparable<Composer> { ... |
Resources
Spring MVC RequestBody annotation and converters
Download a Maven project for the code contained in this (and other) articles. Run the web service first via jetty:run, and then run the unit tests. For the simple raw test, simply right JAXBHelper as a Java application.
You probably might also know that if you have curl installed you can do this:
1 2 3 |
curl -i -H "Accept: application/xml" http://localhost:8080/jaxbmarshal/composerws/Mozart/Wolfgang/Amadeus curl -i -H "Accept: application/json" http://localhost:8080/jaxbmarshal/composerws/composers |
Thank you !!
I have been trying to solve this for 2 days.
the key really was the ->
@XmlAccessorType(value = XmlAccessType.FIELD)
on the class.
“On the class” You mean on the class has relationship many (example, Blog –> Comments, Should I put @XmlAccessorType(value = XmlAccessType.FIELD) on the comments? I try two classes but nothing happened… so sad…
Thanks. It was so useful.
4+ years later…still clutch! Thank you.