Serializing POJOs with Jackson

Jackson's sensible defaults allow developers to skip all configuration in many cases by simply defining classes responsible for serialization. I covered custom serializers with Jackson before, but it is often less effort to avoid custom serializers than create custom serializers. In addition, extending core classes of a library tightly couple an application to that library. That coupling cost may be worth paying, but if it can be avoided easily, why not avoid it?

Example

Jackson has great POJO binding out of the box, which makes serialization a lot simpler by defining classes that are dedicated to writing out and reading in JSON. Consider a locomotive JSON document that looks like this:

{
  "manufacturer": "Baldwin Locomotive Works",
  "year": 1909,
  "owner": "Southern Pacific Railroad",
  "serialNumber": 29064,
  "type": {
    "notation": "2-8-0",
    "name": "Consolidation"
  }
}

This locomotive document would require two POJOs for Jackson's serialization:

package net.sghill.examples.serialization;

public class Locomotive {
    private String manufacturer;
    private Integer year;
    private String owner;
    private Integer serialNumber;
    private Type type;

    public String getManufacturer() { return manufacturer; }
    public void setManufacturer(String manufacturer) { this.manufacturer = manufacturer; }
    public Integer getYear() { return year; }
    public void setYear(Integer year) { this.year = year; }
    public String getOwner() { return owner; }
    public void setOwner(String owner) { this.owner = owner; }
    public Integer getSerialNumber() { return serialNumber; }
    public void setSerialNumber(Integer serialNumber) { this.serialNumber = serialNumber; }
    public Type getType() { return type; }
    public void setType(Type type) { this.type = type; }
}
package net.sghill.examples.serialization;

public class Type {
    private String name;
    private String notation;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getNotation() { return notation; }
    public void setNotation(String notation) { this.notation = notation; }
}

The getters and setters are important here, not the fields. A JSON key of “name” matches up with setName and getName. Jackson automatically strips off the get or set and downcases the first letter of the remaining word. A few quick examples should help make the JavaBean conventions clear:

| JSON key           | expected getter       | expected setter                |
|--------------------|-----------------------|--------------------------------|
| name               | getName()             | setName(String n)              |
| locomotiveNumber   | getLocomotiveNumber() | setLocomotiveNumber(Integer n) |
| flag               | isFlag()              | setFlag(boolean f)             |

Data Types

It's worth the effort to keep these classes simple. On my projects we've tried hard to ensure the serialization objects only have fields with types from java.lang.* or other serialization classes. Jackson already knows how to serialize these types, so sticking to them makes life easy. By avoiding complex types like java.util.Date, there is no more guessing how that should be serialized (millis? ISO 8601?). Instead, define that formatting behavior somewhere easily testable - like a POJO for mapping between domain objects and serialization objects.

ObjectMapper

When using a library or framework that provides Jackson integration out of the box (like Dropwizard, Spring, or Retrofit) we don't have to deal with an ObjectMapper directly. Still, it is often helpful to know how to serialize and deserialize on your own. Using ObjectMapper is straightforward:

// to JSON
String json = new ObjectMapper().writeValueAsString(myLocomotive);

// from JSON
Locomotive l = new ObjectMapper().readValue(myJsonString, Locomotive.class);

The default ObjectMapper assumes the JSON it is reading and writing will be camelCase. It's a good default because that's pretty much always what we want, but there are cases where we need to consume a PascalCase API or write out a snake_case response. For concerns like these, the ObjectMapper can be configured and supplied to the library or framework that needs it. For example, here is how you configure an ObjectMapper to always use snake_case:

new ObjectMapper().setPropertyNamingStrategy(PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy);

Notes

  • The namespaces here are for Jackson 2.0+. Jackson 1.9 can co-exist with 2.0 because everything is provided in a different package.
  • Serialization objects have gone by many different names on my teams:
    • WireTypes
    • Representations
    • Resources
    • Responses

Published Dec 22, 2015