Jackson: More than JSON for Java

Jackson is a mature and feature-rich open source project that we use, support, and contribute to here at Indeed. As Jackson’s creator and primary maintainer, I want to highlight Jackson’s core competencies, extensions, and challenges in this two-part series.

Photo of Cape Flattery by Tatu Saloranta, Jackson's creator

   Photo of Cape Flattery by Tatu Saloranta

Jackson’s core competency

If you’re creating a web service in Java that reads or returns JSON, you need a library to convert Java objects into JSON and vice versa. Since this functionality is not available in the Java standard library (JDK), you can use the Jackson library for its core competency: writing Java objects as JSON and reading JSON as Java objects. Later in this post, I introduce Jackson’s additional features. 

As a Java JSON library, Jackson is:

Jackson is downloaded 25 million times per month via Maven Central, and 16,000 projects depend on jackson-databind. It’s the second most widely used Java library, after Guava, according to the Core Infrastructure Initiative’s census results.

You can use Jackson directly, but nowadays users are more likely to encounter its functionality exposed by libraries and frameworks. These libraries and frameworks use Jackson by default for JSON handling to expose JSON requests and responses; or JSON configuration files as Java objects.

Some examples are:

Users of these frameworks, libraries, and clients may not even be aware that they utilize Jackson. We are more likely to learn this when troubleshooting usage issues or making changes to default handling.

Examples of Jackson usage

The following example shows how to annotate request and response models in the Spring Boot framework.

// Model of updated and accessed content
public class User {
  private Long id;
  private String name;
  @JsonProperty(“dob”) // customize name used in JSON
  private LocalDate dateOfBirth;
  private LocalDateTime lastLogin;
  // plus public Getters, Setters
}

// Service endpoint definition
@RestController
@RequestMapping("/users")
public class UserController {
  // ...
  @GetMapping("/{id}")
  public User findUserById(@PathVariable Long id) {
    return userStorage.findUserById(id);
  }
  @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
  @ResponseStatus(HttpStatus.CREATED)
  public Long createUser(@RequestBody User user) {
    return userStorage.createUser(user);
  }
}

In this example, the Spring Boot framework reads JSON as an instance of the User class. In the POST method, it passes a User instance to a storage handler. It also fetches the User instance via a storage handler for a given ID and writes serialized JSON of that instance. For a full explanation, see Jackson JSON Request and Response Mapping in Spring Boot.

You can also read and write JSON directly via the Jackson API, without any framework or library, as follows:

final ObjectMapper mapper = new ObjectMapper();
// Read JSON as a Java object:
User user;
try (final InputStream in = requestContext.getInputStream()) {
  user = mapper.readValue(in, User.class);
}
// Write a Java object as JSON
try (final OutputStream out = requestContext.getOutputStream()) {
  mapper.writeValue(out, user);
}

Jackson is more than JSON for Java

While Jackson’s core functionality originally started as JSON for Java, it quickly expanded through modules, which are pluggable extension components you can add to the Jackson core. These modules support features that the core does not handle by default. Jackson also has extensions for other JVM languages.

These are some of the types of Jackson modules and extensions that are especially helpful:

  • Data format modules
  • Data type modules
  • Modules for Kotlin, Scala, and Clojure

Data format modules

The data format modules allow you to read and write content that is encoded in formats other than JSON. The low-level encoding details of JSON differ from those of YAML, CSV, or Protobuf. However, much of the higher-level data-binding functionality is similar or identical. The higher-level data binding deals with the structure of Java Objects and expressing them as token streams (or a similar abstraction).

Most of the code in the Jackson core is format-independent and only a small part is truly JSON-format specific. So, these so-called data format modules can easily extend Jackson to read and write content in other data formats. The modules implement low-level streaming from the Jackson API, but they share common data-binding functionality when it comes to converting content to and from Java objects.

In the implementation of these modules, you find factories for streaming parsers and generators. They use a specific factory to construct an ObjectMapper that handles format-specific details, while you only interact with a (mostly) format-agnostic mapper abstraction.

The very first data format module added to Jackson is jackson-dataformat-xml for XML. Supported data formats now include:

The usage for data format modules is similar to the Jackson API JSON usage. You only need to change  JsonMapper (or the generic ObjectMapper) to a format-specific alternative like XmlMapper. There are format-specific features that you can enable and disable. Also, some data formats require additional schema information to map content into JSON-like token representation, such as Avro, CSV and Protobuf. But across all formats, the API usage is similar.

Examples

Compared to the previous example that simply reads and writes JSON, these are alternatives for reading and writing other data formats:

// XML usage is almost identical except it uses a different mapper object
ObjectMapper xmlMapper = new XmlMapper();
String doc = xmlMapper.writeValueAsString(user);
User user = xmlMapper.readValue(doc, User.class);

// YAML usage is almost identical except it uses a different mapper object
ObjectMapper yamlMapper = new YAMLMapper();
byte[] asBytes =  yamlMapper.writeValueAsString(user);
User user2 = yamlMapper.readValue(asBytes, User.class);

// CSV requires Schema for most operations (to convert between property
// names and column positions)
// You can compose the schema manually or generate it from POJO.
CsvMapper csvMapper = new CsvMapper();
CsvSchema userSchema = csvMapper.schemaFor(User.class);
String csvDoc = csvMapper.writer(schema)
.writeValueAsString(user);
User user3 = csvMapper.readerFor(User.class)
.with(schema)
.readValue(csvDoc);

Data type modules

Jackson core allows you to read and write Plain Old Java Objects (POJOs) and most standard JDK types (strings, numbers, booleans, arrays, collections, maps, date, calendar, URLs, and UUIDs). However, many Java projects also use value types defined by third-party libraries, such as Guava, Hibernate and Joda. It doesn’t work well if you handle instances of these value types as simple POJOs, especially collection types. Without explicit support, you would have to implement your own handlers as serializers and deserializers to extend Jackson functionality. It’s a huge undertaking to add such explicit support in Jackson core for even the most common Java libraries and could also lead to problematic dependencies.

To solve this challenge, Jackson added a concept called data type modules. These are extensions built and packaged separately from core Jackson and added to the ObjectMapper instance during construction. Data type modules are released by both the Jackson team and external contributors such as authors of third-party libraries. Authors usually add these modules because they want to solve a specific use case and then share the fruits of their labors with others.

Due to the pluggability of these modules, it is possible to use data type modules with different formats and to mix different value types. For example, you can read and write Hibernate-backed Guava ImmutableLists that contain Joda-defined Period values as JSON, CSV, or XML.

The list of known data type modules is long—see Jackson Portal. Here are some examples:

In addition to the data type module implementations, many frameworks also directly support Jackson data type module usage. In particular, various “immutable values” libraries offer such support, such as:

Modules for Kotlin, Scala, Clojure

If you’re using Jackson, you’re not limited to only using POJOs and JDK types with other JVM languages. Jackson has extensions to handle custom types of many other JVM languages.

The following Jackson modules support Kotlin and Scala.

And for Clojure, there are a few libraries that use Jackson under the hood to implement similar support, such as:

This simplifies interoperability further. It makes it easier to use Java functionality from Kotlin, Scala, Clojure, or vice versa.

What’s next

In my next post, I will share my observations on the challenges that the Jackson project faces and how you can contribute to help.


About the author

Tatu Saloranta is a staff software engineer at Indeed. He leads the team that is integrating the next-generation continuous deployment system. Outside of Indeed, he is best known for his open source activities. Under the handle @cowtowncoder, he has authored many popular open source Java libraries, such as Jackson, Woodstox, lzf compression codec, and Java ClassMate. For a complete list see FasterXML Org.


Cross-posted on Medium.

Tweet about this on TwitterShare on FacebookShare on LinkedInShare on RedditEmail this to someone