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