Tutorial |
5 Minute Tutorial for Jackson
(26-Jun-2009) NOTE:
The most up-to-date version of tutorial is now available as FasterXML Jackson Tutorial . This version will be removed in near future, so please update your links!
Introduction to 3 ways to process Json
Jackson Json processor allows you to read (parse) and write (generate) Json generate using 3 APIs:
- Streaming API lets you read and write Json content with minimal overhead
- org.codehaus.jackson.JsonParser for reading, org.codehaus.jackson.JsonGenerator for writing
- Jackson Streaming API is similar to Stax API .
- Data Binding allows you read Json into regular Java Objects, POJOs/beans, and write such Objects out as Json.
- org.codehaus.jackson.map.ObjectMapper is the class that knows how to bind Json data to/from Java objects.
- Data Binding is similar to JAXB for XML
- Tree Model allows convenient access to Json content through an in-memory tree structure
- As with data binding, org.codehaus.jackson.map.ObjectMapper is the thing to use for building tree from JSON (and writing out as JSON); trees consist of org.codehaus.jackson.map.JsonNode nodes.
- Tree Model is similar to DOM trees for XML
Most other Json processing packages expose just a tree model, or a partial version of data binding. Jackson builds and serializes in-memory data models (Object, Tree) using fully streaming parser/generator components, resulting in unparalleled speed, without sacrificing convenience or correctness.
From usage perspective, these 3 methods can be summarized as:
- Streaming API is the most performant (lowest overhead, fastest read/write)
- Data Binding is the most convenient to use
- Tree Model is the most flexible
Given these properties, let's consider these in the reverse order, starting with what is usually the most natural and convenient method for Java developers: Jackson Data Binding API.
Sample Usage: Data Binding (bean)
Data binding is implemented by ObjectMapper object that operates on streaming parsers and generators. Given sample Bean class (from article at http://www.cowtowncoder.com/blog/archives/2008/12/entry_121.html):
public class User {
public enum Gender { MALE, FEMALE };
public static class Name {
private String _first, _last;
public String getFirst() { return _first; }
public String getLast() { return _last; }
public void setFirst(String s) { _first = s; }
public void setLast(String s) { _last = s; }
}
private Gender _gender;
private Name _name;
private boolean _isVerified;
private byte[] _userImage;
public Name getName() { return _name; }
public boolean isVerified() { return _isVerified; }
public Gender getGender() { return _gender; }
public byte[] getUserImage() { return _userImage; }
public void setName(Name n) { _name = n; }
public void setVerified(boolean b) { _isVerified = b; }
public void setGender(Gender g) { _gender = g; }
public void setUserImage(byte[] b) { _userImage = b; }
}
And sample data like:
{
"name" : { "first" : "Joe", "last" : "Sixpack" },
"gender" : "MALE",
"verified" : false,
"userImage" : "Rm9vYmFyIQ=="
}
Given this, how do we get a User Object instance out of Json, stored in file "user.json"?
Like so:
ObjectMapper mapper = new ObjectMapper(); // can reuse, share globally
User user = mapper.readValue(new File("user.json"), User.class);
Simple? We think so.
But how about the other direction: let's say we modify User instance to mark entry as verified ('user.setVerified(true)'), and want to save data back to the file.
Let's try this:
mapper.writeValue(new File("user-modified.json"), user);
Still looks quite straight-forward.
For more detailed description of how exactly Jackson Data Binding works, check out Data Binding Deep Dive .
Sample Usage: Data Binding ("untyped")
A commonly used alternative ways to do data binding is to use "untyped" binding. Untyped here means that we do not have strict Java classes (types) to bind to, but rather want to construct a loose "poor man's object" (combination of Maps, Lists and wrappers) to operate on. For example, with data from previous example we could do:
HashMap<String,Object> untyped = mapper.readValue(new File("user.json"), HashMap.class);
and get a result Object quite like one we would construct by:
HashMap<String,Object> untyped = new HashMap<String,Object>();
HashMap<String,String> nameStruct = new HashMap<String,String>();
nameStruct.put("first", "Joe");
nameStruct.put("last", "Sixpack");
untyped.put("name", nameStruct);
untyped.put("gender", "MALE");
untyped.put("verified", Boolean.FALSE);
untyped.put("userImage", "Rm9vYmFyIQ==");
Why? Because by specifying HashMap.class, we do not specify generic key/value types. This means that nominal types is Object.class, which in turns makes Jackson use simplest possible types (all of which are of type Object.class, obviously).
Sample Usage: Tree Model
Yet another way to get Objects out of JSON is to build a tree. This is similar to DOM trees for xml. The way Jackson builds trees is to use basic JsonNode base class, which exposes read access that is usually needed. Actual node types used are sub-classes; but the sub-type only needs to be used when modifying trees.
Trees can be read and written using either Streaming API (see below), or using ObjectMapper.
With ObjectMapper, you will do something like:
ObjectMapper m = new ObjectMapper();
// can either use mapper.readTree(JsonParser), or bind to JsonNode
JsonNode rootNode = m.readValue(new File("user.json"), JsonNode.class);
// ensure that "last name" isn't "Xmler"; if is, change to "Jsoner"
JsonNode nameNode = rootNode.path("name");
String lastName = nameNode.path("last").getTextValue().
if ("xmler".equalsIgnoreCase(lastName)) {
((ObjectNode)nameNode).put("last", "Jsoner");
}
// and write it out:
m.writeValue(new File("user-modified.json"), rootNode);
Sample Usage: Streaming API
Just for fun, let's implement the writing functionality (equivalent to earlier examples) using "raw" Streaming API:
JsonFactory f = new JsonFactory();
JsonGenerator g = f.createJsonGenerator(new File("user.json"));
g.writeStartObject();
g.writeObjectFieldStart("name");
g.writeStringField("first", "Joe");
g.writeStringField("last", "Sixpack");
g.writeEndObject(); // for field 'name'
g.writeStringField("gender", Gender.MALE);
g.writeBooleanField("verified", false);
g.writeFieldName("userImage"); // no 'writeBinaryField' (yet?)
byte[] binaryData = ...;
g.writeBinary(binaryData);
g.writeEndObject();
Not horribly bad (esp. compared to amount of work needed for writing, say, equivalent XML content), but certainly more laborious than basic Object mapping.
On the other hand, you do have full control over each and every detail. And overhead is minimal: this is still a bit faster than using ObjectMapper; not a whole lot (perhaps 20-30% faster in common cases), but still.
And perhaps most importantly, output is done in streaming manner: except for some buffering, all content will be written out right away. This means that memory usage is also minimal.
How about parsing, then? Code could look something like:
JsonFactory f = new JsonFactory();
JsonParser jp = f.createJsonParser(new File("user.json"));
User user = new User();
jp.nextToken(); // will return JsonToken.START_OBJECT (verify?)
while (jp.nextToken() != JsonToken.END_OBJECT) {
String fieldname = jp.getCurrentName();
jp.nextToken(); // move to value, or START_OBJECT/START_ARRAY
if ("name".equals(fieldname)) { // contains an object
Name name = new Name();
while (jp.nextToken() != JsonToken.END_OBJECT) {
String namefield = jp.getCurrentName();
jp.nextToken(); // move to value
if ("first".equals(namefield)) {
name.setFirst(jp.getText());
} else if ("last".equals(namefield)) {
name.setLast(jp.getText());
} else {
throw new IllegalStateException("Unrecognized field '"+fieldname+"'!");
}
}
user.setName(name);
} else if ("gender".equals(fieldname)) {
user.setGender(Gender.valueOf(jp.getText()));
} else if ("verified".equals(fieldname)) {
user.setVerified(jp.getCurrentToken() == JsonToken.VALUE_TRUE);
} else if ("userImage".equals(fieldname)) {
user.setUserImage(jp.getBinaryValue());
} else {
throw new IllegalStateException("Unrecognized field '"+fieldname+"'!");
}
}
which is quite a bit more than you'll use with data binding.
One final trick: it is also possible to use data binding and tree model directly from JsonParser and JsonGenerator. To do this, have a look at methods:
- JsonParser.readValueAs()
- JsonParser.readValueAsTree()
- JsonGenerator.writeObject()
- JsonGenerator.writeTree()
which do about what you might expect them to do.