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.