-
Notifications
You must be signed in to change notification settings - Fork 111
Presentation Jackson 2.1 Overview
As of writing this presentation (August 2012), Jackson version 2.1 is nearing its completion. Although some new features may still be added, the core changes are known by now. So let's have a quick look at what goodies Santa will be bringing this August...
- Formatting changes (mostly by using new (2.0)
@JsonFormat
annotation) - Performance improvements
- Ergonomics (simpler, more intuitive methods)
- Usual bug fixes, support for extensions
- Improvements to extension modules
The new @JsonFormat
annotation is getting much more usage with 2.1: with 2.0, the only real use case was to allow customization of DateFormat
s on per-property basis (using 'pattern' property).
But 2.1 adds a new group of usage, where 'shape' property is used to choose an alternative serialization 'style', by specifying what kind of JSON value type should be used.
As name implies, it will be possible to force serialization of Java Enum
s as full JSON Objects (instead of index or name). You can also use @JsonFormat.shape
to choose between name/index use (with Shape.NUMBER
and Shape.STRING
), as an alternative. So:
@JsonFormat(shape=JsonFormat.Shape.OBJECT)
static class MyEnum {
A, B;
public String getFoobar() { return name(); }
}
String json = objectMapper.writeValueAsString(MyEnum.A);
could produce something like:
{ "foobar" : "A" }
NOTE: there is no specific handling for deserialization currently: but you can use @JsonCreator
on a static method, to handle deserialization
One long-standing issue has been that anything that Jackson recognizes to be of type java.util.Collection
, it will automatically get elements and serialize them within a JSON Array.
But sometimes developer just wants the default "POJO serialization" to apply to their Collection
subtype.
If so, this is now easy: just add
@JsonFormat(shape=JsonFormat.Shape.OBJECT)
public class MySpecialSnowflakes extends ArrayList<Snowflake>
and you will get the stock default handling of that class.
This feature works symmetrically, so you should also be able to deserialize same collections (as with POJOs).
Regular POJOs ("beans") are, by default, serialized as JSON Objects. It is also possible to use @JsonValue
to return an alternative presentation Object (like a String
), which gets serialized instead of the value itself: this allows alternative output formats, especially for cases where a simple String or int
might be the most efficient form. This is done for things like java.util.Date
.
But after writing CSV format module, we started thinking that compactness of CSV is kind of nice. But with CSV, one loses ability to do nested data structures. Wouldn't it be nice to use compact JSON arrays, and use position to infer logical property that value maps to?
So, with that, you could convert POJO such as:
@JsonPropertyOrder(alphabetic=true)
public class Point {
public int x, y;
}
by adding annotation
@JsonFormat(shape=JsonFormat.Shape.ARRAY)
and get serialization like:
[12,35]
instead of
{ "x":12,"y":35}
and obviously deserialization works similarly (but styles must match)
This is useful for reducing size of resulting JSON, for cases where compactness matters. And it also helps with...
POJO-as-array is actually a very interesting optional performance optimization on its own: because it eliminates the need to include property names in JSON (or Smile, or other data formats), it reduces both size of serialization AND processing time needed. This means that it is a clear performance win, as well as bandwidth-saver.
Whether you can use this feature obviously depends on who you are interacting with. It is most likely to come in handy for systems where you control both sides of communication. But it could also be an option for JSON-consuming clients; they could perhaps request such behavior.
But there are other small but meaningful improvements that should add up, to make 2.1 the Fastest Jackson Version Ever.
Although Jackson comes with nice support for binary payload (either Base64, as in textual formats like JSON; or native for formats like Smile
), there is one limitation casused by initial API design: you have to read/write the whole value as one big BLOB, regardless of size of that value. This is problematic if you are planning to design a data exchange protocol that deals with hundred-meg sized blobs (like, say, rdist
or such)
Jackson 2.1 extends API to allow incremental reading and writing of binary content; and support is added in JSON and Smile
modules initially (since it is more challenging and usually less useful for others).
New methods are:
JsonParser.readBinaryValue(OutputStream)
JsonGenerator.writeBinary(InputStream, int expectedLength)
It may at first seem odd to see InputStream
and OutputStream
there: but these turn out to work reasonably well for passing chunks of data back and forth, and without requiring new abstractions.
One thing to note is that there is no natural way to expose this via data-binding API. This means that to use this feature, you have to use Streaming API; that is, directly interact with JsonParser
and JsonGenerator
.
I will be using this feature heavily myself, in a distributed system that needs to exchange megabyte-sized chunks between peer storage nodes; and will ensure that performance is as good as underlying mechanism (raw inclusion for Smile
, base64 encoding for JSON) allows.
One small improvement is that when possible, ObjectReader
and ObjectWriter
will try to fetch the matching JsonSerializer
or JsonDeserializer
that is needed, ahead of time, and keep on using it (as long as configuration does not change).
This means that second reading or writing of a value should be bit faster (due to eliminated lookup). This may not be huge win for all cases, but should help in two main cases:
- Processing very short/small JSON documents (or, XML/CSV/Smile/BSON)
- Heavily multi-threaded cases: some locking may be needed for the root handler lookup (due to caching)
Although format auto-detection has been in Jackson since 1.8 (and supported by most data formats by now), so far it has been necessary to bootstrap things using Streaming API. But not any more.
Here is an easy example of auto-detecting whether piece of content is JSON, Smile or XML; and regardless of choice, data-bind it into a POJO:
ObjectMapper mapper = new ObjectMapper();
XmlMapper xmlMapper = new XmlMapper(); // XML is special: must start with its own mapper
ObjectReader reader = mapper
.reader(POJO.class) // for reading instances of POJO
.withFormatDetection(new JsonFactory(), xmlMapper.getFactory(), new SmileFactory();
File f = new File("unknown-data.bin");
return reader.readValue(f);
Neat, eh?
So what happens if content is none of types? You get a JsonProcessingException
indicating this.
Sometimes you need a custom serializer or deserializer, but don't feel like working directly with streaming API (JsonParser
, JsonGenerator
). Instead, wouldn't it be nice if you just converted a tricky type into something Jackson already knows how to handle, and it would just handle the rest?
This is what com.fasterxml.jackson.databind.deser.std.StdDelegatingDeserializer
and com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer
do.
All you have to specify is a Converter
to convert from Java type you are handling ("target type") to or from "delegate type", type that Jackson knows how to handle.
Common delegate types include java.util.Map
and JsonNode
(when dealing with structured types) as well as basic scalar types like String
.
For example, consider we have this POJO class to handle, and are not happy with annotation-based approaches (even mix-in annotations):
public static class Immutable {
protected int x, y;
public Immutable(int x0, int y0) {
x = x0;
y = y0;
}
}
one new way to deserialize it from JSON would be to use following custom deserializer:
new StdDelegatingDeserializer<Immutable>(
new Converter<JsonNode, Immutable>() {
public Immutable convert(JsonNode value) {
int x = value.path("x").asInt();
int y = value.path("y").asInt();
return new Immutable(x, y);
}
}
)
);
which would basically take in value of type JsonNode
(delegate type) -- something that Jackson binds out of JSON being read -- and constructs actual value (of target type). Simple enough.
In reverse direction we could use delegating serializer:
new StdDelegatingSerializer(Immutable.class,
new Converter<Immutable, Map<String,Integer>>() {
public Map<String, Integer> convert(Immutable value) {
HashMap<String,Integer> map = new LinkedHashMap<String,Integer>();
map.put("x", value.x());
map.put("y", value.y());
return map;
}
})
where we use delegate type of java.util.Map
for fun.
Since 2.0, much of innovation has moved from the core components (annotations, streaming, databind) into separate extension modules. Here are some of notable improvements in modules that depend on core 2.1 release.
The most requested feature for Jackson XML module has been the ability to support so-called "unwrapped" Lists. What is meant by this is that given POJO like:
public class POJO {
public List<Point> points;
}
public class Point {
int x, y;
}
where are two ways to represent List
"points"; either as "wrapped" (which Jackson 2.0 did), where there is element for List property, as well as for each value in the List (by default with same name; but wrapper name can differ from value element name)
<POJO> <!-- wrapped notation -->
<points>
<points><x>1</x><y>2</y></points>
<points><x>1</x><y>2</y></points>
</points>
</POJO>
or as "unwrapped", in which there is no XML element matching the List property; instead, only values have their elements:
<POJO> <!-- unwrapped notation -->
<points><x>1</x><y>2</y></points>
<points><x>1</x><y>2</y></points>
</POJO>
Jackson 2.1 will still default to "wrapped" notation; but you have couple of ways to change that:
- Change global default (see below)
- Use JAXB annotations (which default to "unwrapped") introspector
- Use Jackson annotation
Global option works like this:
JacksonXmlModule module = new JacksonXmlModule();
// to default to using "unwrapped" Lists:
module.setDefaultUseWrapper(false);
XmlMapper xmlMapper = new XmlMapper(module);
meaning that you configure module that contains XML features, and use that to construct XmlMapper
instance.
Jackson annotation to use would look like:
public class POJO {
@JacksonXmlElementWrapper(useWrapping=false)
public List<Point> points;
}
// if you did want wrapping, you would instead use something like:
@JacksonXmlElementWrapper("wrapperName")
Aside from this feature here are other improvements:
- JAXB annotation
@XmlIDREF
works more like it does with JAXB provider: that is, it always outputs id value, not POJO serialization, regardless of whether reference predates serialization of the Object. - Ignore attributes of elements for
List
objects -
JacksonXmlModule.setXMLTextElementName()
to allow matching XML text values into named properties (JAXB, for example, defaults to using "value" -- now Jackson can emulate JAXB here as well)