Using JAXB for Java to XML Conversion

Do you know how easy it is to convert java objects to and from XML using JAXB?

“Failure is the condiment that gives success its flavor.” ― Truman Capote

1. Introduction

JAXB (Java Architecture for XML Binding) offers a very easy path to integrate XML into your java application. Using JAXB you can easily serialize java objects as XML, and also load java objects from XML. Let us investigate JAXB for this purpose.

In this article, we implement java bean classes for catalog and book. We have already covered how to use DOM and SAX to process XML with the same example. Now let us see how to use JAXB to do the same.

Here is the sample XML:

<?xml version="1.0"?>
<catalog>
   <book id="bk101">
      <author>Gambardella, Matthew</author>
      <title>XML Developer's Guide</title>
      <genre>Computer</genre>
      <price>44.95</price>
      <publish_date>2000-10-01</publish_date>
      <description>An in-depth look at creating applications 
      with XML.</description>
   </book>
   <book id="bk102">
      <author>Ralls, Kim</author>
      <title>Midnight Rain</title>
...

2. Java Object Model

JAXB uses annotations to control aspects of how the XML is serialized and deserialized. As such, you may need to annotate your classes with the appropriate JAXB annotations to get the exact representation you need.

2.1 Catalog

The java object model for our application consists of two classes: Catalog and Book. Here is the Catalog class annotated with the appropriate JAXB annotations.

package sample;

import java.util.List;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlElement;

@XmlAccessorType(XmlAccessType.FIELD)
public class Catalog
{
    @XmlElement(name = "book")
    private List<Book> books;

    public List<Book> getBooks()
    {
        return books;
    }

    public void setBooks(List<Book> books)
    {
        this.books = books;
    }
}

The @XmlAccessorType annotation is required when used with @XmlElement to avoid the following exception:

Class has two properties of the same name "books"
        this problem is related to the following location:
                at public java.util.List sample.Catalog.getBooks()
                at sample.Catalog
        this problem is related to the following location:
                at private java.util.List sample.Catalog.books
                at sample.Catalog

It tells JAXB to use the field books (with the name book) for (de-)serialization, rather than the property returned by getBooks().

2.2 Book

And here is the class Book referenced by Catalog. Again we use the @XmlAccessorType annotation along with the @XmlAttribute annotation since we want id to be serialized as an XML attribute rather than an element.

Just for illustration, we have also changed the publish_date in XML to pubdate in java. To properly convert between the two, we need the @XmlElement annotation as shown.

package sample;

import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAccessType;

@XmlAccessorType(XmlAccessType.FIELD)
public class Book
{
    @XmlAttribute
    private String id;
    private String author;
    private String title;
    private String genre;
    private BigDecimal price;
    @XmlElement(name = "publish_date")
    private String pubdate;
    private String description;

    public String getId()
    {
        return id;
    }

    public void setId(String id)
    {
        this.id = id;
    }

    public String getAuthor()
    {
        return author;
    }

    public void setAuthor(String author)
    {
        this.author = author;
    }

    public String getTitle()
    {
        return title;
    }

    public void setTitle(String title)
    {
        this.title = title;
    }

    public String getGenre()
    {
        return genre;
    }

    public void setGenre(String genre)
    {
        this.genre = genre;
    }

    public BigDecimal getPrice()
    {
        return price;
    }

    public void setPrice(BigDecimal price)
    {
        this.price = price;
    }

    public String getPubdate()
    {
        return pubdate;
    }

    public void setPubdate(String pubdate)
    {
        this.pubdate = pubdate;
    }

    public String getDescription()
    {
        return description;
    }

    public void setDescription(String description)
    {
        this.description = description;
    }
}

3. Reading and Writing XML

Let us now look at the code required to load XML and create objects from the data. It is as simple as:

String xmlFile = ...;
Catalog catalog = JAXB.unmarshal(new File(xmlFile), Catalog.class);

Writing XML is similarly very simple. To write to STDOUT:

JAXB.marshal(catalog, System.out);

Write to an output file directly using:

JAXB.marshal(catalog, new File("output.xml"));

4. Generate XML from Java Objects

To create XML from java objects is pretty straighforward too. In the following code, we create the entire data model in memory and serialize it to an XML file.

Catalog catalog = new Catalog();
{
    Book book = new Book();
    book.setId("bk101");
    book.setTitle("Pride and Prejudice");
    book.setAuthor("Jane Austen");
    book.setGenre("Romance");
    book.setPrice(new BigDecimal("6.99"));
    book.setPubdate("2010-04-01");
    book.setDescription("\"It is a truth universally acknowledged, that a single man in possession of a good fortune must be in want of a wife.\" So begins Pride and Prejudice, Jane Austen's witty comedy of manners-one of the most popular novels of all time-that features splendidly civilized sparring between the proud Mr. Darcy and the prejudiced Elizabeth Bennet as they play out their spirited courtship in a series of eighteenth-century drawing-room intrigues.");
    catalog.getBooks().add(book);
}
{
    Book book = new Book();
    book.setId("bk102");
    book.setTitle("To Kill a Mockingbird");
    book.setAuthor("Harper Lee");
    book.setGenre("Drama");
    book.setPrice(new BigDecimal("6.39"));
    book.setPubdate("2005-07-05");
    book.setDescription("One of the best-loved stories of all time, To Kill a Mockingbird has been translated into more than forty languages, sold more than forty million copies worldwide, served as the basis for an enormously popular motion picture, and was voted one of the best novels of the twentieth century by librarians across the country. A gripping, heart-wrenching, and wholly remarkable tale of coming-of-age in a South poisoned by virulent prejudice, it views a world of great beauty and savage inequities through the eyes of a young girl, as her father-a crusading local lawyer-risks everything to defend a black man unjustly accused of a terrible crime.");
    catalog.getBooks().add(book);
}
JAXB.marshal(catalog, new File(xmlFile));

5. Using an XmlAdapter for Data Conversion

In the above data model, the publish_date is represented as a String. It should properly be represented as a LocalDate. Let us change the representation and find out what happens.

The Book class is modified as follows:

import java.time.LocalDate;
...
@XmlAccessorType(XmlAccessType.FIELD)
public class Book
{
    ...
    @XmlElement(name = "publish_date")
    private LocalDate pubdate;
    ...

    public LocalDate getPubdate() {
        return pubdate;
    }

    public void setPubdate(LocalDate pubdate) {
        this.pubdate = pubdate;
    }
    ...
}

On trying to load the XML as before, we get the following exception.

INFO: No default constructor found on class java.time.LocalDate
java.lang.NoSuchMethodException: java.time.LocalDate.<init>()
        at java.lang.Class.getConstructor0(Class.java:3082)
        at java.lang.Class.getDeclaredConstructor(Class.java:2178)
        at com.sun.xml.internal.bind.v2.ClassFactory.create0(ClassFactory.java:89)
        at com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl.createInstance(ClassBeanInfoImpl.java:270)
        at com.sun.xml.internal.bind.v2.runtime.unmarshaller.UnmarshallingContext.createInstance(UnmarshallingContext.java:684
)

This error occurs because 1) there is no default contructor on LocalDate 2) the unmarhsaller does not know how to convert a String to LocalDate.

Both of these problems can be addressed by adding an XmlAdapter. This adapter takes care of converting the required type to and from String. Here is the adapter for converting LocalDate.

package sample;

import java.time.LocalDate;
import javax.xml.bind.annotation.adapters.XmlAdapter;

public class LocalDateAdapter extends XmlAdapter<String, LocalDate>
{
    @Override
    public String marshal(LocalDate date)
    {
        if ( date == null ) return null;
        return date.toString();
    }

    @Override
    public LocalDate unmarshal(String string)
    {
        return LocalDate.parse(string);
    }
}

Once the adapter class is created, we need to tell JAXB to use this adapter for conversion. This is done using the @XmlJavaTypeAdapter annotation in the Book class as follows:

...
@XmlAccessorType(XmlAccessType.FIELD)
public class Book
{
    ...
    @XmlElement(name = "publish_date")
    @XmlJavaTypeAdapter(LocalDateAdapter.class)
    private LocalDate pubdate;
    ...
}

With this change, the date field is stored as LocalDate and we get no more exceptions from XML (de-)serialization.

6. Modifying XML Using Java 8 Streams

We will now investigate how to use java 8 streams to update XML. As a first step, let us load our sample XML using the java object model and find those books which cost more than, say $30.

BigDecimal cutoff = new BigDecimal("30.0");
catalog
    .getBooks()
    .stream()
    .filter(book -> book.getPrice().compareTo(cutoff) > 0)
    .forEach(book -> JAXB.marshal( book, System.out));

The output from the above code prints out book elements which satisfy the condition.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<book id="bk101">
    <author>Gambardella, Matthew</author>
    <title>XML Developer's Guide</title>
    <genre>Computer</genre>
    <price>44.95</price>
    <publish_date>2000-10-01</publish_date>
    <description>An in-depth look at creating applications
      with XML.</description>
</book>
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<book id="bk110">
    <author>O'Brien, Tim</author>
    <title>Microsoft .NET: The Programming Bible</title>
    <genre>Computer</genre>
    <price>36.95</price>
...

Let us now apply a 10% discount to those titles which cost more than $30. This is done with the following code:

BigDecimal cutoff = new BigDecimal("30.0");
BigDecimal newPrice = new BigDecimal("0.90");
catalog
    .getBooks()
    .stream()
    .filter(book -> book.getPrice().compareTo(cutoff) > 0)
    .forEach(book -> book.setPrice(book.getPrice().multiply(newPrice)));
JAXB.marshal(catalog, System.out);

Each book entry is modified so that the new price is 90% of the original. The output from this code reflects the prices.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<catalog>
    <book id="bk101">
        <author>Gambardella, Matthew</author>
        <title>XML Developer's Guide</title>
        <genre>Computer</genre>
        <price>40.4550</price>
        <publish_date>2000-10-01</publish_date>
        <description>An in-depth look at creating applications
      with XML.</description>
    </book>
    <book id="bk102">
        <author>Ralls, Kim</author>
        <title>Midnight Rain</title>
        <genre>Fantasy</genre>
        <price>5.95</price>
        <publish_date>2000-12-16</publish_date>
...

Note that books which cost below $30 are unchanged from the original.

Conclusion

We have now learnt the basics of using JAXB to convert between java and XML. We create a java model of objects and use annotations to control the XML output. With this arrangement, it is easy to integrate XML into your java application.