All You Need To Know About JSON Parsing With Jackson

Most of the web today exchanges data in JSON format. Web servers, web and mobile applications, even IoT devices all talk with each other using JSON. Therefore, an easy and flexible way of handling JSON is essential for any software to survive in today’s world.

Example Code

This article is accompanied by a working code example on GitHub.

What is JSON?

JSON stands for “JavaScript Object Notation”, it’s a text-based format for representing structured data based on JavaScript object syntax. Its dynamic and simple format made it extremely popular. In its essence, it follows a key-value map model allowing nested objects and arrays:

{
  "array": [
    1,
    2,
    3
  ],
  "boolean": true,
  "color": "gold",
  "null": null,
  "number": 123,
  "object": {
    "a": "b",
    "c": "d"
  },
  "string": "Hello World"
}

What is Jackson?

Jackson is mainly known as a library that converts JSON strings and Plain Old Java Objects (POJOs). It also supports many other data formats such as CSV, YML, and XML.

Jackson is preferred by many people because of its maturity (13 years old) and its excellent integration with popular frameworks, such as Spring. Moreover, it’s an open-source project that is actively developed and maintained by a wide community.

Under the hood, Jackson has three core packages Streaming, Databind, and Annotations. With those, Jackson offers us three ways to handle JSON-POJO conversion:

Streaming API

It’s the fastest approach of the three and the one with the least overhead. It reads and writes JSON content as discrete events. The API provides a JsonParser that reads JSON into POJOs and a JsonGenerator that writes POJOs into JSON.

Tree Model

The Tree Model creates an in-memory tree representation of the JSON document. An ObjectMapper is responsible for building a tree of JsonNode nodes. It is the most flexible approach as it allows us to traverse the node tree when the JSON document doesn’t map well to a POJO.

Data Binding

It allows us to do conversion between POJOs and JSON documents using property accessors or using annotations. It offers two types of binding:

  • Simple Data Binding which converts JSON to and from Java Maps, Lists, Strings, Numbers, Booleans, and null objects.

  • Full Data Binding which Converts JSON to and from any Java class.

ObjectMapper

ObjectMapper is the most commonly used part of the Jackson library as it’s the easiest way to convert between POJOs and JSON. It lives in com.fasterxml.jackson.databind.

The readValue() method is used to parse (deserialize) JSON from a String, Stream, or File into POJOs.

On the other hand, the writeValue() method is used to turn POJOs into JSON (serialize).

The way ObjectMapper works to figure out which JSON field maps to which POJO field is by matching the names of the JSON fields to the names of the getter and setter methods in the POJO.

That is done by removing the “get” and “set” parts of the names of the getter and setter methods and converting the first character of the remaining method name to lowercase.

For example, say we have a JSON field called name, ObjectMapper will match it with the getter getName() and the setter setName() in the POJO.

ObjectMapper is configurable and we can customize it to our needs either directly from the ObjectMapper instance or by using Jackson annotations as we will see later.

Maven Dependencies

Before we start looking at code, we need to add Jackson Maven dependency jackson-databind which in turn transitively adds jackson-annotations and jackson-core

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.13.3</version>
</dependency>

We are also using Lombok to handle the boilerplate code for getters, setters, and constructors.

Basic JSON Serialization and Deserialization with Jackson

Let’s go through Jackson’s most important use-cases with code examples.

Basic POJO / JSON Conversion Using ObjectMapper

Let’s start by introducing a simple POJO called Employee:

@Getter  
@AllArgsConstructor  
@NoArgsConstructor  
public class Employee {  
    private String firstName;  
    private String lastName;  
    private int age;  
}

Let’s start by turning a POJO to a JSON string:

public class JacksonTest {  
  
  ObjectMapper objectMapper = new ObjectMapper();
  
  @Test  
  void pojoToJsonString() throws JsonProcessingException {  
        Employee employee = new Employee("Mark", "James", 20);  
  
        String json = objectMapper.writeValueAsString(employee);  
  
        System.out.println(json);  
    }  
}

We should see this as output:

{"firstName":"Mark","lastName":"James","age":20}

Now, Let’s see convert a JSON string to an Employee object using the ObjectMapper.

public class JacksonTest {  
  ...
  @Test  
  void jsonStringToPojo() throws JsonProcessingException {  
        String employeeJson = "{\n" +  
                " \"firstName\" : \"Jalil\",\n" +  
                " \"lastName\" : \"Jarjanazy\",\n" +  
                " \"age\" : 30\n" +  
                "}";  
  
        Employee employee = objectMapper.readValue(employeeJson, Employee.class);  
  
        assertThat(employee.getFirstName()).isEqualTo("Jalil");  
    }  
}

The ObjectMapper also offers a rich API to read JSON from different sources into different formats, let’s check the most important ones.

Creating a POJO from a JSON file

This is done using the readValue() method.

JSON file under test resources employee.json:

{  
  "firstName":"Homer",  
  "lastName":"Simpson",  
  "age":44  
}
public class JacksonTest {
	...
	@Test  
	void jsonFileToPojo() throws IOException {  
	    File file = new File("src/test/resources/employee.json");  
	  
	    Employee employee = objectMapper.readValue(file, Employee.class);  
	  
	    assertThat(employee.getAge()).isEqualTo(44);  
	    assertThat(employee.getLastName()).isEqualTo("Simpson");  
	    assertThat(employee.getFirstName()).isEqualTo("Homer");  
	}
}

Creating a POJO from a Byte Array of JSON

public class JacksonTest {
	...
	@Test  
	void byteArrayToPojo() throws IOException {  
	    String employeeJson = "{\n" +  
	            " \"firstName\" : \"Jalil\",\n" +  
	            " \"lastName\" : \"Jarjanazy\",\n" +  
	            " \"age\" : 30\n" +  
	            "}";  
	  
	    Employee employee = objectMapper.readValue(employeeJson.getBytes(), Employee.class);  
	  
	    assertThat(employee.getFirstName()).isEqualTo("Jalil");  
	}
}

Creating a List of POJOs from JSON

Sometimes the JSON document isn’t an object, but a list of objects. Let’s see how we can read that.

employeeList.json:

[  
  {  
    "firstName":"Marge",  
    "lastName":"Simpson",  
    "age":33  
  },  
  {  
    "firstName":"Homer",  
    "lastName":"Simpson",  
    "age":44  
  }  
]
public class JacksonTest {
	...
	@Test 
	void fileToListOfPojos() throws IOException {  
	    File file = new File("src/test/resources/employeeList.json");  
	    List<Employee> employeeList = objectMapper.readValue(file, new TypeReference<>(){});  
	  
	    assertThat(employeeList).hasSize(2);  
	    assertThat(employeeList.get(0).getAge()).isEqualTo(33);  
	    assertThat(employeeList.get(0).getLastName()).isEqualTo("Simpson");  
	    assertThat(employeeList.get(0).getFirstName()).isEqualTo("Marge");  
	}
}

Creating a Map from JSON

We can choose to parse the JSON to a Java Map, which is very convenient if we don’t know what to expect from the JSON file we are trying to parse. ObjectMapper will turn the name of each variable in the JSON to a Map key and the value of that variable to the value of that key.

public class JacksonTest {
	...
	@Test  
	void fileToMap() throws IOException {  
	    File file = new File("src/test/resources/employee.json");  
	    Map<String, Object> employee = objectMapper.readValue(file, new TypeReference<>(){});  
	  
	    assertThat(employee.keySet()).containsExactly("firstName", "lastName", "age");  
	  
	    assertThat(employee.get("firstName")).isEqualTo("Homer");  
	    assertThat(employee.get("lastName")).isEqualTo("Simpson");  
	    assertThat(employee.get("age")).isEqualTo(44);  
	}
}

Ignore Unknown JSON fields

Sometimes the JSON we expect might have some extra fields that are not defined in our POJO. The default behavior for Jackson is to throw a UnrecognizedPropertyException exception in such cases. We can, however, tell Jackson not to stress out about unknown fields and simply ignore them. This is done by configuring ObjectMapper’s FAIL_ON_UNKNOWN_PROPERTIES to false.

employeeWithUnknownProperties.json:

{  
  "firstName":"Homer",  
  "lastName":"Simpson",  
  "age":44,  
  "department": "IT"  
}
public class JacksonTest {
	...
	@Test  
	void fileToPojoWithUnknownProperties() throws IOException {  
	    File file = new File("src/test/resources/employeeWithUnknownProperties.json");  
  	    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);  
	  
	    Employee employee = objectMapper.readValue(file, Employee.class);  
	  
	    assertThat(employee.getFirstName()).isEqualTo("Homer");  
	    assertThat(employee.getLastName()).isEqualTo("Simpson");  
	    assertThat(employee.getAge()).isEqualTo(44);  
	}
}

Working with Dates in Jackson

Date conversions can be tricky as they can be represented with many formats and levels of specification (seconds, milliseconds, etc..).

Date to JSON

Before talking about Jackson and Date conversion, we need to talk about the new Date API provided by Java 8. It was introduced to address the shortcomings of the older java.util.Date and java.util.Calendar. We are mainly interested in using the LocalDate class which offers a powerful way to express date and time.

To do that, we need to add an extra module to Jackson so that it can handle LocalDate.

<dependency>  
    <groupId>com.fasterxml.jackson.datatype</groupId>  
    <artifactId>jackson-datatype-jsr310</artifactId>  
    <version>2.13.3</version>  
</dependency>

Then we need to tell the ObjectMapper to look for and register the new module we’ve just added.

public class JacksonTest {
	ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules();
	...
	@Test  
	void orderToJson() throws JsonProcessingException {  
	    Order order = new Order(1, LocalDate.of(1900,2,1));  
	  
	    String json = objectMapper.writeValueAsString(order);  
	  
	    System.out.println(json);  
	}
}

The default behavior for Jackson then is to show the date as [yyyy-MM-dd] So, the output would be {"id":1,"date":[1900,2,1]}

We can, however, tell Jackson what format we want the date to be. This can be done using the @JsonFormat annotation

public class Order {  
    private int id;  
    @JsonFormat(pattern = "dd/MM/yyyy")  
    private LocalDate date;  
}
@Test  
void orderToJsonWithDate() throws JsonProcessingException {  
    Order order = new Order(1, LocalDate.of(2023, 1, 1));  
  
    String json = objectMapper.writeValueAsString(order);  
  
    System.out.println(json);  
}

This should output {"id":1,"date":"01/01/2023"}.

JSON to Date

We can use the same configuration above to read a JSON field into a date.

order.json:

{  
  "id" : 1,  
  "date" : "30/04/2000"  
}
public class JacksonTest {
	...
	@Test  
	void fileToOrder() throws IOException {  
	    File file = new File("src/test/resources/order.json");  
	  
	    Order order = objectMapper.readValue(file, Order.class);  
	  
	    assertThat(order.getDate().getYear()).isEqualTo(2000);  
	    assertThat(order.getDate().getMonthValue()).isEqualTo(4);  
	    assertThat(order.getDate().getDayOfMonth()).isEqualTo(30);  
	}
}

Jackson Annotations

Annotations in Jackson play a major role in customizing how the JSON/POJO conversion process takes place. We have seen an example of it with the date conversion where we used the @JsonFormat annotation. Annotations mainly affect how the data is read, written or even both. Let’s explore some of those annotations based on their categories.

Read Annotations

They affect how Jackson converts JSON into POJOs.

@JsonSetter

This is useful when we want to match a field in the JSON string to a field in the POJO where their names don’t match.

@NoArgsConstructor  
@AllArgsConstructor  
@Getter  
public class Car {  
    @JsonSetter("carBrand")  
    private String brand;  
}
{  
  "carBrand" : "BMW"  
}
public class JacksonTest {
	...
	@Test  
	void fileToCar() throws IOException {  
	    File file = new File("src/test/resources/car.json");  
	  
	    Car car = objectMapper.readValue(file, Car.class);  
	  
	    assertThat(car.getBrand()).isEqualTo("BMW");  
	}
}

@JsonAnySetter

This annotation is useful for cases where the JSON contains some fields that are not declared in the POJO. It is used with a setter method that is called for every unrecognized field.

public class Car {  
    @JsonSetter("carBrand")  
    private String brand;  
    private Map<String, String> unrecognizedFields = new HashMap<>();  
  
    @JsonAnySetter  
    public void allSetter(String fieldName, String fieldValue) {  
        unrecognizedFields.put(fieldName, fieldValue);  
    }  
}

carUnrecognized.json file:

{  
  "carBrand" : "BMW",  
  "productionYear": 1996  
}
public class JacksonTest {
	...
	@Test  
	void fileToUnrecognizedCar() throws IOException {  
	    File file = new File("src/test/resources/carUnrecognized.json");  
	  
	    Car car = objectMapper.readValue(file, Car.class);  
	  
	    assertThat(car.getUnrecognizedFields()).containsKey("productionYear");  
	}
}

Write Annotations

They affect how Jackson converts POJOs into JSON.

@JsonGetter

This is useful when we want to map a POJOs field to a JSON field using a different name. For example, say we have this Cat class with the field name, but we want its JSON name to be catName.

@NoArgsConstructor  
@AllArgsConstructor  
public class Cat {  
    private String name;  
  
    @JsonGetter("catName")  
    public String getName() {  
        return name;  
    }  
}
public class JacksonTest {
	...
	@Test  
	void catToJson() throws JsonProcessingException {  
	    Cat cat = new Cat("Monica");  
	  
	    String json = objectMapper.writeValueAsString(cat);  
	  
	    System.out.println(json);  
	}
}

This will output

{
	"catName":"Monica"
}

@JsonAnyGetter

This annotation allows us to treat a Map object as a source of JSON properties. Say we have this map as a field in the Cat class

@NoArgsConstructor  
@AllArgsConstructor  
public class Cat {  
      private String name;  
  
	  @JsonAnyGetter  
	  Map<String, String> map = Map.of(  
	            "name", "Jack",  
	  "surname", "wolfskin"  
	  );
  ...
  }
@Test  
void catToJsonWithMap() throws JsonProcessingException {  
    Cat cat = new Cat("Monica");  
  
   String json = objectMapper.writeValueAsString(cat);  
  
   System.out.println(json);  
}

Then this will output

{
  "catName":"Monica",
  "name":"Jack",
  "surname":"wolfskin"
}

Read/Write Annotations

Those annotations affect both reading and writing a JSON.

@JsonIgnore

The annotated filed is ignored while both writing and reading JSON.

@AllArgsConstructor  
@NoArgsConstructor  
@Getter  
public class Dog {  
    private String name;  
    @JsonIgnore  
	private int age;  
}
public class JacksonTest {
	...
	@Test  
	void dogToJson() throws JsonProcessingException {  
	    Dog dog = new Dog("Max", 3);  
	  
	    String json = objectMapper.writeValueAsString(dog);  
	  
	    System.out.println(json);  
	}
}

This will print out {"name":"Max"}

The same applies to reading into a POJO as well.

Say we have this dog.json file:

{  
  "name" : "bobby",  
  "age" : 5  
}
public  class  JacksonTest  { 
	 ...
	@Test  
	void fileToDog() throws IOException {  
	    File file = new File("src/test/resources/dog.json");  
	  
	    Dog dog = objectMapper.readValue(file, Dog.class);  
	  
	    assertThat(dog.getName()).isEqualTo("bobby");  
	    assertThat(dog.getAge()).isNull();  
	}
}

Jackson has many more useful annotations that give us more control over the serialization/deserialization process. The full list of them can be found on Jackson’s Github repository.

Summary

  • Jackson is one of the most powerful and popular libraries for JSON processing in Java.

  • Jackson consists of three main modules Streaming API, Tree Model, and Data Binding.

  • Jackson provides an ObjectMapper which is highly configurable to suit our needs through setting its properties and also using annotations.

You can find all the example code in the GitHub repo.

Abdulcelil Cercenazi

Software developer and craftsman. Loves working with Java | Spring | Docker. Always looking for new adventures

Recent Posts

Testing Time-Based Features with Feature Flags

Time-based features in a software application are a pain to test. To test such a feature, you can (and should) write unit tests, of course.

Read more
Getting Started with AWS Step Functions

Getting Started with AWS Step Functions

AWS Step Functions is a serverless orchestration service by which we can combine AWS Lambda functions and other AWS services to build complex business applications.

Read more

Node.js Logging with Winston

Logging is used to provide accurate context about what occurs in our application, it is the documentation of all events that happen within an application.

Read more