Three ways to use Jackson for JSON in Java

May 07, 2020
Written by

If you’re working in a statically-typed language like Java then dealing with JSON can be tricky. JSON doesn’t have type definitions and is lacking some features which we would like - there’s only strings, numbers, booleans and null, so to store other types (like dates or times) we’re forced to use a string-based convention. Despite its shortcomings, JSON is the most common format for APIs on the web so we need a way to work with it in Java.

Jackson is one of the most popular Java JSON libraries, and is the one I use most frequently. In this post I’ll pick a fairly complex JSON document and three queries which I want to make using Jackson. I’ll compare three different approaches:

  1. Tree model
  2. Data binding
  3. Path queries

All the code used in this post is in this repository. It’ll work with Java 8 onwards.

Other Java Libraries for working with JSON

The most popular Java libraries for working with JSON, as measured by usage in maven central and GitHub stars, are Jackson and Gson. In this post I will be using Jackson, and there is an equivalent post with Gson code examples here.

You can see the Jackson dependency for the examples here.

Example data and questions

To find some example data I read Tilde’s recent post 7 cool APIs you didn’t know you needed, and picked out the Near Earth Object Web Service API from the NASA APIs. This API is maintained by the brilliantly-named SpaceRocks team.

The NeoWS Feed API request returns a list of all asteroids whose closest approach to Earth is within the next 7 days. I’ll be showing how answer the following questions in Java:

  • How many are there?
    This can be found by looking at the element_count key at the root of the JSON object.
  • How many of them are potentially hazardous?
    We need to loop through each NEO and check the is_potentially_hazardous_asteroid key, which is a boolean value in the JSON. (Spoiler: it’s not zero 😨)
  • What is the name and speed of the fastest Near Earth Object?
    Again, we need to loop through but this time the objects are more complex. We also need to be aware that speeds are stored as strings not numbers, eg "kilometers_per_second": "6.076659807". This is common in JSON documents as it avoids precision issues on very small or very large numbers.

A tree model for JSON

Jackson allows you to read JSON into a tree model: Java objects that represent JSON objects, arrays and values. These objects are called things like JsonNode or JsonArray and are provided by Jackson.

Pros:

  • You will not need to create any extra classes of your own
  • Jackson can do some implicit and explicit type coercions for you

Cons:

  • Your code that works with Jackson’s tree model objects can be verbose
  • It’s very tempting to mix Jackson code with application logic which can make reading and testing your code hard

Jackson tree model examples

Jackson uses a class called ObjectMapper as its main entrypoint. Typically I create a new ObjectMapper at application startup and because ObjectMapper instances are thread-safe it’s OK to treat it like a singleton.

An ObjectMapper can read JSON from a variety of sources using an overloaded readTree method. In this case I provided a String. readTree returns a JsonNode which represents the root of the JSON document. JsonNode instances can be JsonObjects, JsonArrays or a variety of “value” nodes such as TextNode or IntNode.

Here is the code to parse a String of JSON into a JsonNode:

ObjectMapper mapper = new ObjectMapper();
JsonNode neoJsonNode = mapper.readTree(SourceData.asString());

[this code in the example repo]

How many NEOs are there?

We need to find the element_count key in the JsonNode, and return it as an int. The code reads quite naturally:

int getNeoCount(JsonNode neoJsonNode) {
   return neoJsonNode
       .get("element_count")
       .asInt();
}

Error handling: if element_count is missing then .get("element_count") will return null and there will be a NullPointerException at .asInt(). Jackson can use the Null Object Pattern to avoid this kind of exception, if you use .path instead of .get. Either way you will have to handle errors, so I tend to use .get.

How many potentially hazardous asteroids are there this week?

I admit that I expected the answer here to be zero. It’s currently 19 - but I’m not panicking (yet). To calculate this from the root JsonNode we need to:

  • iterate through all the NEOs - there is a list of these for each date so we will need a nested loop
  • increment a counter if the is_potentially_hazardous_asteroid field is true

Here’s the code:

int getPotentiallyHazardousAsteroidCount(JsonNode neoJsonNode) {
   int potentiallyHazardousAsteroidCount = 0;
   JsonNode nearEarthObjects = neoJsonNode.path("near_earth_objects");
   for (JsonNode neoClosestApproachDate : nearEarthObjects) {
       for (JsonNode neo : neoClosestApproachDate) {
           if (neo.get("is_potentially_hazardous_asteroid").asBoolean()) {
               potentiallyHazardousAsteroidCount += 1;
           }
       }
   }
   return potentiallyHazardousAsteroidCount;
}

[this code in the example repo]

This is a little awkward - Jackson does not directly allow us to use the Streams API so I’m using nested for loops. asBoolean returns the value for boolean fields in the JSON but can also be called on other types:

  • numeric nodes will resolve as true if nonzero
  • text nodes are true if the value is "true".

What is the name and speed of the fastest NEO?

The method of finding and iterating through each NEO is the same as the previous example, but each NEO has the speed nested a few levels deep so you need to descend through to pick out the kilometers_per_second value.

"close_approach_data": [
 {
    ...
     "relative_velocity": {
         "kilometers_per_second": "6.076659807",
         "kilometers_per_hour": "21875.9753053124",
         "miles_per_hour": "13592.8803223482"
   },
  ...
 }
]

I created a small class to hold both values called NeoNameAndSpeed. This could be a record in the future. The code creates one of those objects like this:

NeoNameAndSpeed getFastestNEO(JsonNode neoJsonNode) {
   NeoNameAndSpeed fastestNEO = null;
   JsonNode nearEarthObjects = neoJsonNode.path("near_earth_objects");
   for (JsonNode neoClosestApproachDate : nearEarthObjects) {
       for (JsonNode neo : neoClosestApproachDate) {
           double speed = neo
               .get("close_approach_data")
               .get(0)
               .get("relative_velocity")
               .get("kilometers_per_second")
               .asDouble();
           if ( fastestNEO == null ||  speed > fastestNEO.speed ){
               fastestNEO = new NeoNameAndSpeed(neo.get("name").asText(), speed);
           }
       }
   }
   return fastestNEO;
}

[this code in the example repo]

Even though the speeds are stored as strings in the JSON I could call .asDouble() - Jackson is smart enough to call Double.parseDouble for me.

Data binding JSON to custom classes

If you have more complex queries of your data, or you need to create objects from JSON that you can pass to other code, the tree model isn’t a good fit. Jackson offers another mode of operation called data binding, where JSON is parsed directly into objects of your design. By default Spring MVC uses Jackson in this way when you accept or return objects from your web controllers.

Pros:

  • JSON to object conversion is straightforward
  • reading values out of the objects can use any Java API
  • the objects are independent of Jackson so can be used in other contexts
  • the mapping is customizable using Jackson Modules

Cons:

  • Up-front work: you have to create classes whose structure matches the JSON objects, then have Jackson read your JSON into these objects.

Data binding 101

Here’s a simple example based off a small subset of the NEO JSON:

{
 "id": "54016476",
 "name": "(2020 GR1)",
 "closeApproachDate": "2020-04-12",
}

We could imagine a class for holding that data like this:

class NeoSummaryDetails {
    public int id;
    public String name;
    public LocalDate closeApproachDate;
}

Jackson is almost able to map back and forth between JSON and matching objects like this out of the box. It copes fine with the int id actually being a string, but needs some help converting the String 2020-04-12 to a LocalDate object. This is done with a custom module, which defines a mapping from JSON to custom object types.

Jackson data binding - custom types

For the LocalDate mapping Jackson provides a dependency. Add this to your project and configure your ObjectMapper like this:

   ObjectMapper objectMapper = new ObjectMapper()
       .registerModule(new JavaTimeModule());

[this code in the example repo]

Jackson data binding - custom field names

You might have noticed that I used closeApproachDate in my example JSON above, where the data from NASA has close_approach_date. I did that because Jackson will use Java’s reflection capabilities to match JSON keys to Java field names, and they need to match exactly.

Most times you can’t change your JSON - it’s usually coming from an API that you don’t control - but you still wouldn’t like to have fields in your Java classes written in snake_case. This could have been done with an annotation on the closeApproachDate field:

@JsonProperty("close_approach_date")
public LocalDate closeApproachDate;

[this code in the example repo]

Creating your custom objects with JsonSchema2Pojo

Right now you are probably thinking that this can get very time-consuming. Field renaming, custom readers and writers, not to mention the sheer number of classes you might need to create.  Well, you’re right! But fear not, there’s a great tool to create the classes for you.

JsonSchema2Pojo can take a JSON schema or (more usefully) a JSON document and generate matching classes for you. It knows about Jackson annotations, and has tons of options, although the defaults are sensible. Usually I find it does 90% of the work for me, but the classes often need some finessing once they’re generated.

To use it for this project I removed all but one of the NEOs and selected the following options:

JsonSchema2Pojo screenshot

[generated code in the example repo]

Data stored in keys and values

The NeoWS JSON has a slightly awkward (but not unusual) feature - some data is stored in the keys rather than the values of the JSON objects. The near_earth_objects map has keys which are dates. This is a bit of a problem because the dates won’t always be the same, but of course jsonschema2pojo doesn’t know this. It created a field called _20200412. To fix this I renamed the class _20200412 to NeoDetails and the type of nearEarthObjects became Map<String, List<NeoDetails>> (see that here). I could then delete the now-unused NearEarthObjects class.

I also changed the types of numbers-in-strings from String to double and added LocalDate where appropriate.

Jackson data binding for the Near-Earth Object API

With the classes generated by JsonSchema2Pojo the whole big JSON document can be read with:

NeoWsDataJackson neoWsDataJackson = new ObjectMapper()
   .registerModule(new JavaTimeModule())
   .readValue(SourceData.asString(), NeoWsDataJackson.class);

Finding the data we want

Now that we have plain old Java objects we can use field access and the Streams API to find the data we want:

System.out.println("NEO count: " + neoWsData.elementCount);


System.out.println("Potentially hazardous asteroids: " +
   neoWsData.nearEarthObjects.values()
       .stream().flatMap(Collection::stream) // this converts a Collection of Collections of objects into a single stream
       .filter(neo -> neo.isPotentiallyHazardousAsteroid)
       .count());


NeoDetails fastestNeo = neoWsData.nearEarthObjects.values()
   .stream().flatMap(Collection::stream)
   .max( Comparator.comparing( neo -> neo.closeApproachData.get(0).relativeVelocity.kilometersPerSecond ))
   .get();

System.out.println(String.format("Fastest NEO is: %s at %f km/sec",
   fastestNeo.name,
   fastestNeo.closeApproachData.get(0).relativeVelocity.kilometersPerSecond));

[this code in the example repo]

This code is more natural Java, and it doesn’t have Jackson all through it so it would be easier to unit test this version. If you are working with the same format of JSON a lot, the investment of creating classes is likely to be well worth it.

Path queries from JSON using JsonPointer

With Jackson you can also use a JSON Pointer which is a compact way to refer to a specific single value in a JSON document:

JsonNode node = objectMapper.readTree(SourceData.asString());

JsonPointer pointer = JsonPointer.compile("/element_count");

System.out.println("NEO count: " + node.at(pointer).asText());

[this code in the example repo]

JSON Pointers can only point to a single value - you can’t aggregate or use wildcards, so they are rather limited.

Summing up the different ways of using Jackson

For simple queries, the tree model can serve you well but you will most likely be mixing up JSON parsing and application logic which can make it hard to test and maintain.

To pull out a single value from a JSON document, you might consider using a JSON pointer, but the code is barely any simpler than using the tree model so I never do.

For more complex queries, and especially when your JSON parsing is part of a larger application, I recommend data binding. It’s usually easiest in the long run, remembering that JsonSchema2Pojo can do most of the work for you.

What are your favourite ways of working with JSON in Java? Let me know on Twitter I’m @MaximumGilliard, or by email I am mgilliard@twilio.com. I can’t wait to see what {"you": "build"}.