Skip to content

Using JAXB

Christoph Beck edited this page Jan 7, 2012 · 21 revisions

StAXON makes JSON look like XML on the Java side. One benefit from this is that you can integrate JSON with powerful XML-related technologies for free. The Java XML Binding API is a widely used standard and can be easily adopted for use with JSON.

To make JAXB-bindings to JSON as easy as possible, StAXON provides the ability to configure the marshal and unmarshal process with the @JsonXML annotation. It has the following properties:

  • autoArray (boolean) - whether to automatically add JSON array boundaries (default is false)
  • virtualRoot (boolean) - whether the JSON strips the root element (default is false)
  • prettyPrint (boolean) - whether to format output for better readability (default is false)
  • namespaceSeparator (char) - character used as namespace prefix separator (default is ':')
  • namespaceDeclarations (boolean) - whether to write namespace declarations (default is true)
  • multiplePaths (String[]) - absolute or relative (suffix) paths to array elements (default is none)

A JsonXMLMapper<T> instance can then be used to read/write JSON objects or arrays of a JAXB-annotated model type (@XmlRootElement or @XmlType) T via the following methods:

  • T readObject(Reader reader) throws JAXBException, XMLStreamException
  • void writeObject(Writer writer, T value) throws JAXBException, XMLStreamException
  • List<T> readArray(Reader reader) throws JAXBException, XMLStreamException
  • void writeArray(Writer writer, Collection<T> collection) throws JAXBException, XMLStreamException

The mapper will detect a JsonXML annotation on type T if present.

Twitter Example

This sample has been inspired by JSON Binding with EclipseLink MOXy - Twitter Example by Blaise Doughan.

Our sample request we'll send over to Twitter is http://search.twitter.com/search.json?q=%23JAXB to search for tweets containing the #JAXB hashtag. Then, we'll use StAXON and JAXB to

  • unmarshal the JSON response
  • modify the resulting Java model
  • marshal the model back to JSON

First of all, we need a JAXB-annotated model.

Model

For the sake of simplicity, our model classes only contain a subset of the properties provided by the Twitter Search API. The root element class will be SearchResults, which contains a list of Result objects. Finally, the DateAdapter class is used to parse and format java.util.Date values.

SearchResults.java

import java.util.List;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

import de.odysseus.staxon.json.jaxb.JsonXML;

@JsonXML(
	virtualRoot = true,         // JSON will omit "customer" root
	multiplePaths = "/results", // trigger JSON array for "phone" list
	prettyPrint = true)         // produce formatted JSON output
@XmlRootElement
public class SearchResults {
	private String query;
	private float completedIn;
	private List<Result> results;

	public String getQuery() {
		return query;
	}
	public void setQuery(String query) {
		this.query = query;
	}

	@XmlElement(name = "completed_in")
	public float getCompletedIn() {
		return completedIn;
	}
	public void setCompletedIn(float completedIn) {
		this.completedIn = completedIn;
	}

	public List<Result> getResults() {
		return results;
	}
	public void setResults(List<Result> results) {
		this.results = results;
	}
}

Result.java

import java.util.Date;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

public class Result {
	private Date createdAt;
	private String fromUser;
	private String text;

	@XmlElement(name = "created_at")
	@XmlJavaTypeAdapter(DateAdapter.class)
	public Date getCreatedAt() {
		return createdAt;
	}
	public void setCreatedAt(Date createdAt) {
		this.createdAt = createdAt;
	}

	@XmlElement(name = "from_user")
	public String getFromUser() {
		return fromUser;
	}
	public void setFromUser(String fromUser) {
		this.fromUser = fromUser;
	}

	public String getText() {
		return text;
	}
	public void setText(String text) {
		this.text = text;
	}
}

DateAdapter.java

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

import javax.xml.bind.annotation.adapters.XmlAdapter;

public class DateAdapter extends XmlAdapter<String, Date> {
	private SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z");

	@Override
	public String marshal(Date date) {
		return dateFormat.format(date);
	}

	@Override
	public Date unmarshal(String string) throws ParseException {
		return dateFormat.parse(string);
	}
}

Demo

Now that we have our model, we are ready to create and run some sample code.

TwitterDemo.java

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Date;

import javax.xml.bind.JAXBException;
import javax.xml.stream.XMLStreamException;

import de.odysseus.staxon.json.jaxb.JsonXMLMapper;

/**
 * Twitter JAXB demo.
 */
public class TwitterDemo {
	/**
	 * Read/write JSON returned from Twitter search using JAXB.
	 * @param args ignored
	 * @throws IOException
	 * @throws XMLStreamException
	 * @throws JAXBException
	 */
	public static void main(String[] args) throws IOException, XMLStreamException, JAXBException {
		/*
		 * Search for tweets containing the #JAXB" hashtag.
		 */
		InputStream input = new URL("http://search.twitter.com/search.json?q=%23JAXB").openStream();

		/*
		 * Create mapper instance.
		 */
		JsonXMLMapper<SearchResults> mapper = new JsonXMLMapper<SearchResults>(SearchResults.class);
		
		/*
		 * Read tweets.
		 */
		SearchResults searchResults = mapper.readObject(input);
		input.close();

		/*
		 * Add an item for fun...
		 */
		Result result = new Result();
		result.setCreatedAt(new Date());
		result.setFromUser("ChristophBeck");
		result.setText("You can do #JAXB with StAXON too:)");
		searchResults.getResults().add(0, result);

		/*
		 * Write back to console.
		 */
		mapper.writeObject(System.out, searchResults);
	}
}

Running the sample will produce something like this:

{
  "completed_in" : "0.071",
  "query" : "%23JAXB",
  "results" : [ {
    "created_at" : "Sat, 17 Sep 2011 21:45:33 +0200",
    "from_user" : "me",
    "text" : "You can do #JAXB with StAXON too:)"
  },

  remvoved some items...

  {
    "created_at" : "Fri, 12 Aug 2011 15:06:43 -0400",
    "from_user" : "bdoughan",
    "text" : "You can now use EclipseLink JAXB (MOXy) with JSON :)"
  } ]
}

Demo, the hard way

Consider that - for some reason - we do not want to (or cannot) use the @JsonXML and JsonXMLMapper mechanism. In this case, we have to configure our StAX readers and writers manually and interact with JAXB directly.

In order to trigger a JSON array for our SearchResults.results list, we are decorating our writer with an XMLMultipleStreamWriter and pass it the element path to our list as an array path:

xmlStreamWriter = new XMLMultipleStreamWriter(xmlStreamWriter, "/results");

The wrapper triggers a JSON array start by inserting an <?xml-multiple?> processing instruction into the stream before writing a sequence of elements matching one of its array paths. See Mastering Arrays for more on dealing with JSON arrays.

Everything else is straight JAXB!

TwitterDemo.java

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.Date;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;

import de.odysseus.staxon.json.JsonXMLConfig;
import de.odysseus.staxon.json.JsonXMLConfigBuilder;
import de.odysseus.staxon.json.JsonXMLInputFactory;
import de.odysseus.staxon.json.JsonXMLOutputFactory;
import de.odysseus.staxon.json.util.XMLMultipleStreamWriter;

/**
 * Twitter JAXB demo.
 */
public class TwitterDemo {
	/**
	 * Unmarshal/marshal JSON returned from Twitter search using JAXB.
	 * @param args ignored
	 * @throws IOException
	 * @throws XMLStreamException
	 * @throws JAXBException
	 */
	public static void main(String[] args) throws IOException, XMLStreamException, JAXBException {
		/*
		 * Search for tweets containing the #JAXB" hashtag.
		 */
		URL url = new URL("http://search.twitter.com/search.json?q=%23JAXB");
		
		/*
		 * The JSON object returned from twitter does not include the "searchResults" root,
		 * so we specify it as "virtual" root element. When reading, the virtual root will be
		 * added to the stream. When writing, the "virtual" root will be removed from the stream.
		 */
		JsonXMLConfig config = new JsonXMLConfigBuilder().
				virtualRoot("searchResults").
				prettyPrint(true).
				build();

		/*
		 * Obtain a StAX reader from StAXON.
		 */
		InputStream input = url.openStream();
		XMLStreamReader reader =  new JsonXMLInputFactory(config).createXMLStreamReader(input);

		/*
		 * Create JAXB context.
		 */
		JAXBContext context = JAXBContext.newInstance(SearchResults.class);

		/*
		 * Unmarshal using JAXB.
		 */
		SearchResults searchResults = (SearchResults) context.createUnmarshaller().unmarshal(reader);

		/*
		 * As per StAX specification, XMLStreamReader.close() doesn't close the
		 * underlying stream.
		 */
		input.close();

		/*
		 * Add an item for fun...
		 */
		Result result = new Result();
		result.setCreatedAt(new Date());
		result.setFromUser("ChristophBeck");
		result.setText("You can do #JAXB with StAXON too:)");
		searchResults.getResults().add(0, result);

		/*
		 * Obtain a StAX writer from StAXON.
		 */
		OutputStream output = System.out;
		XMLStreamWriter writer = new JsonXMLOutputFactory(config).createXMLStreamWriter(output);
		
		/*
		 * To insert JSON array boundaries, we wrap our writer into an XMLMultipleStreamWriter.
		 */
		writer = new XMLMultipleStreamWriter(writer, false, "/results");

		/*
		 * Marshal using JAXB.
		 */
		context.createMarshaller().marshal(searchResults, writer);
	}
}