Skip to content

Presentation Jackson 2.0

willf edited this page Aug 9, 2012 · 9 revisions

Jackson 2.0 Feature Overview

Jackson 2.0 is the first Major release after official 1.x versions. As the major version bump indicates, 2.0 is the first official release that is NOT fully backwards compatible.

This presentation gives an overview of changes that occured between versions 1.9 and 2.0.

Background: why bump major version?

Although progression from 1.9 to 2.0 seems natural from "increasing numbers" perspective, we did actually seriously consider going to 1.10 and possibly more versions, with the usual rules of backwards compatibility.

But after so many minor releases, Jackson had accumulated some technical debt, in form of deprecated methods, as well as many non-intuitive naming choices and non-optimal packaging decisions. Further, external developments like awesomeness of GitHub and steady improvement of Maven build system were suggesting that we might want to improve building and release management aspects as well; changes that would cause some backwards-compatibility issues of their own.

None of the issues alone would have been sufficient to justify break from expected compatibility (in our opinion); but taken as whole, it seemed like time would be ripe to make a clean break, to pay down most of the technical debt.

The Cleanup ("project Spring cleaning")

The rough summary of clean-up changes that started Jackson 2.0 development is:

  1. Moving code, projects to GitHub
  1. Change build from Ant to Maven
  • Simplifies releases significantly; can deploy to Sonatype OSS Maven repository (which syncs to central Maven repo)
  1. Remove ALL methods, types, annotations that were deprecated in 1.9!
  2. Rename/move
  • Rename Java packages, mostly to allow 1.x and 2.0 to co-exist; in some cases to make more intuitive
  • Rename a small number of misnamed classes (for example DeserializationConfig.Feature as DeserializationFeature)
  • Move couple of classes to clean up package structure (split "too big" packages)

New feature: full handling of Object Graphs

Throughout development of Jackson 1.x, one feature proved too hard to solve: that of automatically serializing and deserializing object graphs that may contain cyclic references. Although partial solutions have been added -- Jackson 1.6 added support for so-called back references, which allows handling of parent/child type of references -- they have limited applicability.

One big part of the problem is figuring out what inclusion mechanism to use: since JSON does not have 'natural' mechanism for embedding metadata (XML, for example, makes it somewhat natural to use attributes for metadata, and elements for data), one has to consider carefully details of adding metadata (like Object Id) along with actual data (object properties).

With 2.0, we decided to go back to the drawing board and think of existing features that might help model Object Identity; and realized that the closest applicable is the way that Type Information is handled. As a result, one new annotation -- @JsonIdentityInfo -- was added to indicate types of Objects for which Object Identity handling is enabled, as well as details of how Id values are produced.

The way @JsonIdentityInfo works is as follows:

  • Object Id is always included as a property (with configurable name). This means that output shape must be JSON Object; and means that currently identity of Java Collections, Arrays and Maps can not be handled
  • First instance of an Object is serialized completely (including Object Id); further references are serialized using Object Id itself
  • Object ids can be either generated (Jackson creates ids on the fly), as either UUIDs or sequence numbers (ints); or provided, in which case Object itself provides the value through property (field value or getter).

An example annotation looks like:

@JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="@id")
public class Identifiable {
  public String name;
  public Identifiable bestFriend;
}

and could produce JSON output like:

{
  "@id" : 1,
  "name" : "Bob",
  "bestFriend" : {
     "@id" : 2,
     "name" : "Bill"
     "bestFriend" : 1
  }
}

for case where two Identifiable objects were referencing each other.

This case uses 'generated' id, using basic int sequence: the main difference from 'provided' case is that the "id property" that annotation presents does NOT have actual property in class. An alternative POJO definition could be:

@JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
public class Identifiable2 {
  public String name;
  public Identifiable bestFriend;
  public int id; // or getter/setter; instances must initialize value for serialization
}

in which case class itself needs to provide the id value, and Jackson will use it as-is.

Limitations to handling are currently:

  • Types handled must be POJOs; Collections, arrays and Maps can not use Object Identity currently
  • It may be possible to add support for these types in future (tricky, but possible)
  • No "default Object Id" (unlike with Type Ids): each type must be annotated, directly or using mix-ins; or use custom AnnotationIntrospector to indicate types for which Object Ids are to be used
  • JAXB XmlID annotation is supported (via JAXB module)

Other Jackson 2.0 features

Support Builder-style objects

Builder-style of construction has been gaining popularity in Java world; Jackson itself uses similar approach for creating and configuring ObjectReader and ObjectWriter instances.

An example of how to support Builder-style construction:

@JsonDeserialize(builder=ValueBuilder.class) // important!
public class Value {
  private final int x, y;
  protected Value(int x, int y) {
    this.x = x;
    this.y = y;
  }
}
public class ValueBuilder {
  private int x, y;

  // can use @JsonCreator to use non-default ctor, inject values etc
  public ValueBuilder() { }

  // if name is "withXxx", works as is: otherwise use @JsonProperty("x") or @JsonSetter("x")!
  public ValueBuilder withX(int x) {
    this.x = x;
    return this; // or, construct new instance, return that
  }
  public ValueBuilder withY(int y) {
    this.y = y;
    return this;
  }
  public Value build() {
    return new Value(x, y);
  }
}

so the important thing is to add annotation in POJO being created; and it needs to point to Builder class. The default naming convention assumes "withXxx()" method, but this is configurable.

JsonView for deserialization

Jackson 1.x only supported use of JSON Views for serialization: 2.0 adds symmetric support for deserialization; active view defined via ObjectReader (similar to ObjectWriter for serialization)

Usage can look like:

Value v = mapper
 .reader(Value.class)
 .withView(MyView.class) 
 .readValue(json);

which also demonstrates New and Improved way to construct and configure ObjectReaders (and ObjectWriter works similarly).

Annotation Bundles

One simple but "ergonomic" improvement is ability to use @JacksonAnnotationInside meta-annotation, which causes target annotation to be consider a "bundle", meaning that annotations it has will be applied as if they were directly used on the target class. End result is that you can basically create your own annotations to be used in place of a set of Jackson annotations, to simplify configuration as well as to hide direct Jackson annotation dependencies.

@Retention(RetentionPolicy.RUNTIME) // required by JDK to retain during runtime
@JacksonAnnotationsInside
@JsonInclude(Include.NON_NULL) // only include non-null properties
@JsonPropertyOrder({ "id", "name" })
private @interface StdAnnotations

and then you can do:

@StdAnnotations
public class Value { }

@JsonUnwrapped.prefix, .suffix

Annotation @JsonUnwrapped was added in 1.9, but a significant new addition is inclusion of "prefix" and "suffix" properties. They can be used like:

public class Box {
  @JsonUnwrapped(prefix="topLeft") Point tl;
  @JsonUnwrapped(prefix="bottomRight") Point br;
}
public class Point {
  int x, y;
}

to produce JSON like:

{
 "topLeft.x" : 0,
 "topLeft.y" : 0,
 "bottomRight.x" : 100,
 "bottomRight.y" : 80  
}

(and obviously handle the same when deserializing)

JAX-RS module annotations

With 2.0 version of JAX-RS JSON Provider, it is finally possible to use some annotations on JAX-RS Resource methods:

  • @JsonView (core Jackson annotation) can be used to define JSON View to use for serialization, deserialization
  • @JSONP annotation, provided by module, can be used to indicate whether JSONP is used for serialization, and configuring how (method name to use)
  • @JacksonFeatures can be used to explicitly enable and/or disable DeserializationFeature and SerializationFeature setting.
  • Use it like this:

    public class MyResource {
      // ...
      @JacksonFeatures(deserializationDisable={ DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES })
      @POST // and other JAX-RS annotations
      public void postConfig(ConfigObject ob) {
        // will not throw an exception on encountering unknown properties
      }
    }

@JsonIgnoreProperties as property annotation

In addition to allowing ignoring of specific properties when serializing/deserializing particular class, it is now possibly to add additional properties to be excluded when serializing POJO-valued properties:

public class Pojo {
  @JsonIgnoreProperties({"id"})
  public class SomeObject; // will be serialized without 'id' property
}

annotation will be added to list of properties to ignore for the value type (if any).

Improved config for ObjectReader/ObjectWriter

Starting with 2.0, it is finally possible to configure DeserializationFeature and SerializationFeature settings on per-call basis, cleanly and safely. But additionally everything that is dynamically changeable (meaning changing effects actual operation -- some things are not dynamically changeable as they change actual cached serializers; one example being mix-in annotations) can now be changed with withXxx() methods that ObjectReader and ObjectWriter add.

For example; to write "JAXB compatible" structure, where root-name wrapping is used (but only on some cases), you could do:

String json = writer
  .with(SerializationFeature.WRAP_ROOT_VALUE)
  .withRootName("wrapper")
  .writeValueAsString(value);

and get expected results. Check out Javadocs for all the options.

JsonSerializer.isEmpty()

Another nice if minor improvement is the ability to let (custom) JsonSerializer implementations to determine whether a given value is considered "empty", for purpose of optional filtering of empty values. By default null references are considered empty, as well as a small number of general cases (empty Collections, arrays, Maps). But this extension mechanism can be used to support any other type that has some concept of "empty".

Jackson 2.0 Data format support

Although Jackson 1.x had introduced some non-JSON backends (notably Smile and XML), one significant area for improvement for 2.0 API was to support even wider range of alternate data formats.

Here is a high-level overview of all data formats that Jackson 2.0 supports, either directly or via extension modules

  1. JSON -- the baseline format that Jackson supports out-of-the-box
  2. Smile (since 1.6) -- 100% feature parity, compatibility with JSON at Streaming API level
  • Very mature, stable; extensive production deployments (used by Elastic Search, for example)
  1. BSON (since ~1.7): support for BSON, JSON-like data format used by MongoDB
  • external project (not under FasterXML), implemented by Michel Kramer (with help from Jackson team)
  • Wide usage, considered stable
  1. XML (since 1.8)
  • Builds on standard STAX API: i.e. parsing/generation using Woodstox or Aalto
  • High-performance, slightly faster than JAXB (using same underlying parser/generator), although not up to JSON speed (+50% time spent?)
  • Is being adopted more and more: XML is the most challenging "non-JSON" format to support, but we are getting there as a good alternative to JAXB
  • Due to XML/JSON impedance, requires use of XmlMapper, in addition to XmlFactory -- also means that use of Streaming API with XML is not necessarily useful (just use standard XML STAX API)
  1. CSV (1.9 (preliminary), 2.0 (full)
  • Considered biggest potential growth area: basic implementation exists
  • Reasonably efficient: limited testing suggests speed similar to XML or better (much due to more compact output)
  • Requires use of new Schema object, which simply defines Name/Column mapping; can construct from POJO, or programmatically
  • Needs users! (feedback, suggestions!)
  1. YAML (since 2.0): uses SnakeYAML library for parsing.
  • Newest addition: relatively easy addition, should be more mature than age implies
  • Performance significant lower than that of JSON, XML or CSV: limited by SnakeYAML which works well, correctly, but not very efficiently.

Future plans:

  • Avro? (we have some ideas, project)
  • Thrift? (possible -- interesting to see if IDL could be reused)
  • Protobuf (clean spec, compact, gold standard for speed -- doable)