Introduction
Jackson is a powerful and efficient Java library that handles the serialization and deserialization of Java objects and their JSON representations. It's one of the most widely used libraries for this task, and runs under the hood of many other frameworks. For instance, while the Spring Framework has support for various serialization/deserialization libraries, Jackson is the default engine.
In today's era, JSON is by far the most common and preferred way to produce and consume data by RESTFul web services, and the process is instrumental to all web services. While Java SE does not provide extensive support for converting JSON to Java objects or the other way around, we have third party libraries like Jackson to take care of this for us.
If you'd like to learn more about another useful Java library, Gson - read our guide to Convert Java Object (POJO) To and from JSON with Gson!
That being said - Jackson is one of the "must know" tools for practically all Java software engineers working on web applications, and being familiar/comfortable with it will help you in the long run.
In this in-depth guide, we'll perform a deep dive into the central API of Jackson - the ObjectMapper, giving you a holistic yet detailed view of how you can use the class through many practical examples. Then, we'll take a look at the Tree Model for parsing arbitrary structures, followed by customization flags and writing custom serializers and deserializers.
Installing Jackson
Let's start out by including Jackson as a dependency for our project. If you don't already have one, you can easily generate it via the CLI and Maven:
$ mvn archetype:generate -DgroupId=com.stackabuse.tutorial -DartifactId=objectmapper-tutorial -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=false
Or, use Spring Initializr to create a skeleton project through a GUI. Jackson isn't a built-in dependency, so you can't include it by default either from the CLI or Spring Initializr, however, including it is as easy as modifying your pom.xml
file with:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.1</version>
</dependency>
Or, if you're using Gradle as your build tool:
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.1'
This installs two libraries: jackson-annotations
and jackson-core
.
Introduction of ObjectMapper Class
The main class in the Jackson library for reading and writing JSON is ObjectMapper
. It's in the com.fasterxml.jackson.databind
package and can serialize and deserialize two types of objects:
- Plain Old Java Objects (POJOs)
- General-purpose JSON Tree Models
If you already have a domain class, a POJO, you can convert between that class and JSON by providing the class to the ObjectMapper
. Alternatively, you can convert any arbitrary JSON into any arbitrary JSON Tree Model in case you don't have a specialized class for the conversion or if it's "uneconomical" to make one.
The ObjectMapper
class provides four constructors to create an instance, the following one being the simplest:
ObjectMapper objectMapper = new ObjectMapper();
Here are some of the important features of ObjectMapper
:
- It is thread-safe.
- It serves as a factory for more advanced
ObjectReader
andObjectWriter
classes. JsonParser
andJsonGenerator
objects will be used by the mapper to implement the actual reading and writing of JSON.
The methods available in ObjectMapper
are extensive, so let's get started!
Converting JSON to Java Objects
Arguably one of the two most used features is the conversion of JSON Strings to Java Objects. This is typically done when you receive a response containing a JSON-serialized entity, and would like to convert it to an object for further use.
With
ObjectMapper
, to convert a JSON string into a Java Object, we use thereadValue()
method.
The method accepts a wide variety of data sources, which we'll go through in the upcoming sections.
Convert JSON String to Java Object (POJO)
The simplest form of input is a String
- or rather, JSON-formatted Strings:
<T> T readValue(String content, Class<T> valueType)
Consider the following HealthWorker
class in a Health Management System:
public class HealthWorker {
private int id;
private String name;
private String qualification;
private Double yearsOfExperience;
// Constructor, getters, setters, toString()
}
To convert a JSON String representation of this class into a Java class, we simply supply the string to the readValue()
method, alongside the .class
of the class we're trying to convert to:
ObjectMapper objectMapper = new ObjectMapper();
String healthWorkerJSON = "{\"id\":1,\"name\":\"RehamMuzzamil\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5}";
HealthWorker healthWorker = objectMapper.readValue(healthWorkerJSON, HealthWorker.class);
As you might expect, the healthWorker
object's name
property would be set to "RehamMuzzamil", qualification
to "MBBS" and yearsOfExperience
to 1.5.
Note: The field names must fully match the fields in the JSON string, lest the mapper throw an error. Additionally, they must have valid public getters and setters. Jackson also supports the use of aliases for differing names, which can be used to map any JSON field to any POJO field with a simple annotation.
@JsonAlias and @JsonProperty
Whenever there's a mismatch between the names of properties/fields in a JSON String and a POJO - you can deal with the mismatch by not deserializing them or by "adapting" which JSON fields are mapped to which object fields.
This can be achieved through @JsonAlias
and @JsonProperty
:
- @JsonProperty corresponds to the field names during serialization and deserialization.
- @JsonAlias corresponds to the alternative names during deserialization.
For instance, a common mismatch happens with capitalization conventions - an API may return snake_case
while you're expecting CamelCase
:
public class HealthWorker {
private int workerId;
private String workerName;
private String workerQualification;
private Double yearsOfExperience;
// Constructor, getters, setters and toString()
}
While the incoming JSON looks like this:
{
"worker_id" : 1,
"worker_name" : "RehamMuzzamil",
"worker_qualification" : "MBBS",
"years_of_experience" :1.5
}
These would all be unrecognized fields, even though they obviously represent the same properties! This is easily avoided by setting the @JsonProperty
annotation:
public class HealthWorker {
@JsonProperty("worker_id")
private int workerId;
@JsonProperty("worker_name")
private String workerName;
@JsonProperty("worker_qualification")
private String workerQualification;
@JsonProperty("years_of_experience")
private Double yearsOfExperience;
// Constructor, getters, setters and toString()
}
Now both when serializing and deserializing, the snake case would be enforced, and no issues arise between the POJO and incoming JSON. On the other hand, if you don't want to serialize the fields in snake case, but still be able to read them - you may opt for an alias instead! Incoming snake case would be parsed into camel case, but when you serialize, it'd still be serialized in camel case.
Additionally, you can use both annotations! In this context, the @JsonAlias
would serve as alternative names to be accepted besides the enforced property name, and you can even supply a list to the annotation:
public class HealthWorker {
@JsonProperty("worker_id")
@JsonAlias({"id", "workerId", "identification"})
private int workerId;
@JsonProperty("worker_name")
@JsonAlias({"name", "wName"})
private String workerName;
@JsonProperty("worker_qualification")
@JsonAlias({"workerQualification", "qual", "qualification"})
private String workerQualification;
@JsonProperty("years_of_experience")
@JsonAlias({"yoe", "yearsOfExperience", "experience"})
private Double yearsOfExperience;
// Constructor, getters, setters and toString()
}
Now, any of the aliases would get mapped to the same property, but when serializing, the @JsonProperty
value would be used. You could map multiple API responses to a single object this way, if the APIs contain the same structural response, with different names, for instance.
Convert JSON String to Java Object (POJO) with Readers
A Reader
class represents an arbitrary character stream of data, and can be constructed from sources like Strings. The readValue()
method also accepts a Reader
instead of Strings:
<T> T readValue(Reader src, Class<T> valueType)
The rest of the code is much the same:
ObjectMapper objectMapper = new ObjectMapper();
String healthWorkerJSON = "{\"id\":1,\"name\":\"RehamMuzzamil\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5}";
Reader reader = new StringReader(healthWorkerJSON);
HealthWorker healthWorker = objectMapper.readValue(reader, HealthWorker.class);
Convert JSON File to Java Object (POJO)
JSON doesn't only come in String format - sometimes, it's stored in a file. JSON can be used to format the properties of a configuration file (which can be loaded in a configuration object to set the state of the application), for instance.
The readValue()
function can map JSON data from a file directly to an object, by accepting a File
as well:
<T> T readValue(File src, Class<T> valueType)
The API doesn't change much - you load the file in and pass it into the readValue()
method:
ObjectMapper objectMapper = new ObjectMapper();
File file = new File("<path-to-file>/HealthWorker.json");
HealthWorker healthWorker = objectMapper.readValue(file, HealthWorker.class);
Note: This works the same if you use a FileReader
object instead of a File
object.
Convert JSON to Java Object (POJO) from HTTP Response/URL
JSON was created to be a data-interchange format, particularly for web applications. Again, it's the most prevalent format for data serialization over the web. While you could retrieve the result, save it as a String and then convert using the readValue()
method - you can directly read the HTTP response, given a URL, and deserialize it to the desired class:
<T> T readValue(URL src, Class<T> valueType)
With this approach, you can skip the intermediary String, and directly parse HTTP request results!
Let's consider a Weather Forecast Management System where we rely on the data shared by a web service from the Meteorological Department:
String API_KEY = "552xxxxxxxxxxxxxxxxx122&";
String URLString = "http://api.weatherapi.com/v1/astronomy.json?key="+API_KEY+"q=London&dt=2021-12-30\n";
URL url = new URL(URLString); // Create a URL object, don't just use a URL as a String
ObjectMapper objectMapper = new ObjectMapper();
Astronomy astronomy = objectMapper.readValue(url, Astronomy.class);
Here's a snapshot of what our astronomy
object will contain:
Again, the Astronomy
class just mirrors the expected JSON structure.
Convert JSON InputStream to Java Object (POJO)
The InputStream
represents any arbitrary stream of bytes, and isn't an uncommon format to receive data in. Naturally, ObjectMapper
can also read an InputStream
and map the incoming data to a target class:
<T> T readValue(InputStream src, Class<T> valueType)
For instance, let's convert JSON data from a FileInputStream
:
ObjectMapper objectMapper = new ObjectMapper();
InputStream inputStream = new FileInputStream("<path-to-file>/HealthWorker.json");
HealthWorker healthWorker = objectMapper.readValue(inputStream, HealthWorker.class);
Convert JSON Byte Array to Java Object (POJO)
JSON Byte Arrays can be used to store data, most commonly as blobs (in say, a relational database such as PostgreSQL or MySQL). In another runtime, that blob is retrieved and deserialized back into an object. The BLOB
data type is of particular importance as it is commonly used by a variety of applications, including message brokers, to store the binary information of a file.
The readValue()
method of the ObjectMapper
class can also be used to read byte arrays:
<T> T readValue(byte[] src, Class<T> valueType)
If you have JSON data as a byte array (byte[]
), you'll map it just as you would usually:
ObjectMapper objectMapper = new ObjectMapper();
String healthWorkerJSON = "{\"id\":1,\"name\":\"RehamMuzzamil\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5}";
// Ensure UTF-8 format
byte[] jsonByteArray = healthWorkerJSON.getBytes("UTF-8");
HealthWorker healthWorker = objectMapper.readValue(jsonByteArray, HealthWorker.class);
Convert JSON Array to Java Object Array or List
Reading data from a JSON array and converting it to an array or list of Java objects is another use case - you don't only search for single resources. It uses the same signature as reading a single object:
<T> T readValue(String content, TypeReference<T> valueTypeRef)
As long as the JSON contains an array, we can map it to an array of objects:
String healthWorkersJsonArray = "[{\"id\":1,\"name\":\"RehamMuzzamil\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5},{\"id\":2,\"name\":\"MichaelJohn\",\"qualification\":\"FCPS\",\"yearsOfExperience\":5}]";
ObjectMapper objectMapper = new ObjectMapper();
HealthWorker[] healthWorkerArray = objectMapper.readValue(healthWorkersJsonArray, HealthWorker[].class);
// OR
HealthWorker[] healthWorkerArray = objectMapper.readValue(jsonKeyValuePair, new TypeReference<HealthWorker[]>(){});
Though, since arrays are messy to work with - you can just as easily convert the JSON array into a List of objects:
String healthWorkersJsonArray = "[{\"id\":1,\"name\":\"RehamMuzzamil\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5},{\"id\":2,\"name\":\"MichaelJohn\",\"qualification\":\"FCPS\",\"yearsOfExperience\":5}]";
ObjectMapper objectMapper = new ObjectMapper();
List<HealthWorker> healthWorkerList = objectMapper.readValue(healthWorkersJsonArray, new TypeReference<List<HealthWorker>(){});
Convert JSON String to Java Map
The Map
class is used to store key-value pairs in Java. JSON objects are key-value pairs, so mapping from one to the other is a natural fit!
<T> T readValue(String content, TypeReference<T> valueTypeRef)
We can convert JSON data into a Map
object, with the JSON key corresponding to the map's key, and the JSON's value corresponding to the map's value as easily as:
String jsonKeyValuePair = "{\"TeamPolioVaccine\":10,\"TeamMMRVaccine\":19}";
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> jsonMap = objectMapper.readValue(jsonKeyValuePair, new TypeReference<HashMap>(){});
// OR
Map<String, Object> jsonMap = objectMapper.readValue(jsonKeyValuePair, HashMap.class);
This Map
would contain:
{TeamPolioVaccine=10, TeamMMRVaccine=19}
Convert Java Objects (POJOs) to JSON
We've seen many ways and input sources that can represent JSON data, and how to convert that data into a predefined Java class. Now, let's turn the stick the other way around and take a look at how to serialize Java objects into JSON data!
Similar to the converse conversion - the
writeValue()
method is used to serialize Java objects into JSON.
You can write objects to a string, file or output stream.
Convert Java Object to JSON String
Again, the simplest form your object can be serialized to is a JSON-formatted string:
String writeValueAsString(Object value)
Alternatively, and more rarely, you can write it to a file:
void writeValue(File resultFile, Object value)
There's less variety here, since most of the variety can arise on the receiving end. Let's write a HealthWorker
into JSON:
ObjectMapper objectMapper = new ObjectMapper();
HealthWorker healthWorker = createHealthWorker();
// Write object into a File
objectMapper.writeValue(new File("healthWorkerJsonOutput.json"),healthWorker);
// Write object into a String
String healthWorkerJSON = objectMapper.writeValueAsString(healthWorker);
System.out.println(healthWorkerJSON);
private static HealthWorker createHealthWorker() {
HealthWorker healthWorker = new HealthWorker();
healthWorker.setId(1);
healthWorker.setName("Dr. John");
healthWorker.setQualification("FCPS");
healthWorker.setYearsOfExperience(5.0);
return healthWorker;
}
healthWorkerJsonOutput.json
was created in the current directory with the following contents:
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
{
"id": 1,
"name": "Dr. John",
"qualification": "FCPS",
"yearsOfExperience": 5.0
}
Convert Java Object to FileOutputStream
When saving objects to a JSON file - the contents are internally converted into a FileOutputStream
before being saved, and you can use an OuputStream
directly instead:
void writeValue(OutputStream out, Object value)
The API works in much the same way as previously seen:
ObjectMapper objectMapper = new ObjectMapper();
HealthWorker healthWorker = createHealthWorker();
objectMapper.writeValue(new FileOutputStream("output-health-workers.json"), healthWorker);
This would result in a file, output-health-workers.json
, containing:
{
"id": 1,
"name": "Dr. John",
"qualification": "FCPS",
"yearsOfExperience": 5.0
}
Jackson's JSON Tree Model - Unknown JSON Structures
A JSON object can be represented using Jackson's built-in tree model instead of predefined classes as well. Jackson's Tree Model is helpful when we don't know how the receiving JSON will look or we can't design a class to represent it effectively.
Overview of JsonNode
JsonNode
is a base class for all JSON nodes, which constitutes the foundation of Jackson's JSON Tree Model. It resides in the package com.fasterxml.jackson.databind.JsonNode
.
Jackson can read JSON into a JsonNode
instance and write JSON to JsonNode
using the ObjectMapper
class. By definition, JsonNode
is an abstract class that can not be directly instantiated. However, there are 19 sub-classes of JsonNode
we can use to create objects!
Convert Java Object to JsonNode Using ObjectMapper
The ObjectMapper
class provides two methods that binds data from a Java Object to a JSON tree:
<T extends JsonNode> T valueToTree(Object fromValue)
As well as:
<T> T convertValue(Object fromValue, Class<T> toValueType)
In this guide we'll use valueToTree()
. It's similar to serializing values into JSON, but it's more efficient. The following example demonstrates how we can convert an object to a JsonNode
:
ObjectMapper objectMapper = new ObjectMapper();
HealthWorkerService healthWorkerService = new HealthWorkerService();
HealthWorker healthWorker = healthWorkerService.findHealthWorkerById(1);
JsonNode healthWorkerJsonNode = objectMapper.valueToTree(healthWorker);
Convert JsonNode to Object Using ObjectMapper
The ObjectMapper
class also provides two convenience methods that bind data from a JSON tree to another type (typically a POJO):
<T> T treeToValue(TreeNode n, Class<T> valueType)
And:
<T> T convertValue(Object fromValue, Class<T> toValueType)
In this guide we'll be using treeToValue()
. The following code demonstrates how you can convert JSON to an object, by first converting it to a JsonNode
object:
String healthWorkerJSON = "{\n\t\"id\": null,\n\t\"name\": \"Reham Muzzamil\",\n\t\"qualification\": \"MBBS\",\n\t\"yearsOfExperience\": 1.5\n}";
ObjectMapper objectMapper = new ObjectMapper();
JsonNode healthWorkerJsonNode = objectMapper.readTree(healthWorkerJSON);
HealthWorker healthWorker = objectMapper.treeToValue(healthWorkerJsonNode, HealthWorker.class);
Configuring ObjectMapper's Serialization and Deserialization
The input JSON may differ from or be incompatible with the target POJO by the Jackson API's default deserialization technique. Here are a few examples:
- A JSON string's fields aren't available in the associated POJO.
- In a JSON string, fields of primitive types have null values.
Both of these cases are very common, and you'll generally want to be able to deal with them. Thankfully, both are easy to recover from! There are also situations where we want to manage the customization throughout the serialization process, such as
- Use textual format to serialize
Date
objects instead of timestamps. - Control the behavior of the serialization process when no accessors are found for a particular type.
In these cases, we can configure the ObjectMapper
object to change its behavior. The configure()
method allows us to change the default serialization and deserialization methods:
ObjectMapper configure(SerializationFeature f, boolean state)
ObjectMapper configure(DeserializationFeature f, boolean state)
There's an extensive list of properties, and we'll take a look at the more pertinent ones. They all have sensible defaults - you won't have to change them in most cases, but in more specific circumstances, it's very useful to know which ones you can change.
FAIL_ON_EMPTY_BEANS
The FAIL_ON_EMPTY_BEANS
serialization feature defines what happens when no accessors (properties) for a type are found. If enabled (the default), an exception is thrown to indicate that the bean is non-serializable. If disabled, a bean is serialized as an empty Object with no properties.
We'll want to disable the feature in scenarios such as when a class only has configuration-related imports and no property fields, but in some cases, this exception may "trip you up" if you're working with an object without public methods/properties, resulting in an unwanted exception.
Let's consider an empty Java class:
class SoftwareEngineer {}
The ObjectMapper
class throws the following exception when trying to serialize a class without properties:
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.stackabuse.tutorial.SoftwareEngineer and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
In the context of this scenario, disabling the feature is helpful to process serialization smoothly. The following code snippet demonstrates how to disable this serialization property:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
System.out.println(objectMapper.writeValueAsString(new SoftwareEngineer()));
The execution of the above code snippet results in an empty Object.
{}
WRITE_DATES_AS_TIMESTAMPS
Dates can be written in a myriad of formats, and formatting dates differs from country to country. The WRITE_DATES_AS_TIMESTAMPS
feature defines whether you'd like to write the date field as a numeric timestamp or as another type.
By default, the feature is set to true
, since that's a very universal way to represent a date - and the aforementioned myriad of formats can be derived more easily from a timestamp than other formats. Alternatively, you may want to force a more user-friendly format:
Date date = Calendar.getInstance().getTime();
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
String dateString = dateFormat.format(date);
System.out.println(dateString);
ObjectMapper objectMapper = new ObjectMapper();
System.out.println(objectMapper.writeValueAsString(date));
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
System.out.println(objectMapper.writeValueAsString(date));
Running the code above would give us this output:
2022-01-01 08:34:55
1641051295217
"2022-01-01T15:34:55.217+00:00"
FAIL_ON_UNKNOWN_PROPERTIES
If the JSON string contains fields that are unfamiliar to the POJO, whether it's just a single String
field or more, the deserialization process throws a UnrecognizedPropertyException
. What if we don't care about capturing every data field?
When working with third party APIs you can expect the JSON responses to change through time. Most commonly, these changes aren't announced, so a new property might silently appear and it would break your code! The fix is easy - just add the new property to your POJO. In some cases though, this would entail updating other classes, DTOs, Resource classes, etc. just because a third party added a property that might not be relevant to you.
This is why, the
FAIL_ON_UNKNOWN_PROPERTIES
is set tofalse
by default, and Jackson will just ignore the new properties if they are present.
On the other hand, you might want to force response solidarity within a project - to standardize the data being transmitted between APIs, instead of Jackson silently ignoring properties if they're (erroneously) changed. This would "alert" you to any changes that are being made:
ObjectMapper objectMapper = new ObjectMapper(); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
String healthWorkerJsonUpdated = "{\"id\":1,\"name\":\"RehamMuzzamil\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5,\"specialization\":\"Peadiatrics\"}";
HealthWorker healthWorker = objectMapper.readValue(healthWorkerJsonUpdated, HealthWorker.class);
The above code introduces an unknown property specialization
in the JSON string. Running it would result in the following exception:
Exception in thread "main" com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "specialization" (class com.stackabuse.model.HealthWorker), not marked as ignorable (4 known properties: "id", "qualification", "name", "yearsOfExperience"])
Note: Setting this property to true
would affect all POJOs being created by the ObjectMapper
instance. To avoid this more "global" configuration, we can add this annotation at a class level: @JsonIgnoreProperties(ignoreUnknown = true)
.
FAIL_ON_NULL_FOR_PRIMITIVES
The FAIL_ON_NULL_FOR_PRIMITIVES
feature determines whether to fail when encountering JSON properties as null
while deserializing into Java primitive types (like int
or double
). By default, null values for primitive fields are ignored. However, we can configure the ObjectMapper
to fail instead, in the case that an omission of those fields signals a larger error.
The following code enables this deserialization feature:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, true);
String healthWorkerJSON = "{\"id\":null,\"name\":\"RehamMuzzamil\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5}";
HealthWorker healthWorker = objectMapper.readValue(healthWorkerJSON, HealthWorker.class);
This would result in:
Exception in thread "main" com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot map `null` into type `int` (set DeserializationConfig.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES to 'false' to allow)
ACCEPT_EMPTY_STRING_AS_NULL_OBJECT
When we want to allow or disallow JSON empty string values ""
to be bound to POJOs as null
, we can configure this property. By default, this functionality is turned on.
To demonstrate the use of this deserialization feature, we have modified our HealthWorker
class as follows:
public class HealthWorker {
private int id;
private String name;
private String qualification;
private Double yearsOfExperience;
private Specialization specialization;
// Constructor, getters, setters, toString()
}
It now has a property called specialization
, which is defined as:
public class Specialization {
private String specializationField;
// Constructor, getters, setters, toString()
}
Let's map some input JSON to a HealthWorker
object:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
String healthWorkerJSON = "{\"id\":1,\"name\":\"\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5,\"specialization\":\"\"}";
HealthWorker healthWorker = objectMapper.readValue(healthWorkerJSON, HealthWorker.class);
System.out.println(healthWorker.getSpecialization());
This results in:
null
Create a Custom Serializer and Deserializer with Jackson
Earlier, we encountered a mismatch between JSON String fields and Java Object fields, which are easily "adapted" to each other via annotations. However, sometimes, the mismatch is structural, not semantic.
The ObjectMapper
class allows you to register a custom serializer or deserializer for these cases. This feature is helpful when the JSON structure is different than the Java POJO class into which it has to be serialized or deserialized.
Why? Well, you may want to use data from JSON or class as a different type. For example, an API may provide a number but in your code, you'd like to work with it as a string.
Before we were able to customize serializers and deserializers easily, it would be common for developers to use Data Transfer Objects (DTOs) - classes to interact with the API - which would then be used to populate our POJOs:
If you'd like to read more about DTOs - read our Guide to The Data Transfer Object Pattern in Java - Implementation and Mapping!
Custom serializers allow us to skip that step. Let's dive in!
Implementing a Custom Jackson Serializer
Let's implement a few serializers to get a feel for how they can be used. This serializer takes a native DateTime
value and formats it to a reader/API friendly string:
public class CustomJodaDateTimeSerializer extends StdSerializer<DateTime> {
private static DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm");
public CustomJodaDateTimeSerializer() {
this(null);
}
public CustomJodaDateTimeSerializer(Class<DateTime> t) {
super(t);
}
@Override
public void serialize(DateTime value, JsonGenerator jsonGenerator, SerializerProvider arg2) throws IOException {
jsonGenerator.writeString(formatter.print(value));
}
}
This serializer converts a double value (for example, a price in dollars and cents) into a string:
public class DoubleToStringCustomSerializer extends JsonSerializer<Double> {
@Override
public void serialize(Double value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(value.toString());
}
}
This serializer returns a JSON object based on a HealthWorker
object's data. Note the change from the Java object's name
property and the JSON's full_name
:
public class HealthWorkerCustomSerializer extends StdSerializer<HealthWorker> {
private static final long serialVersionUID = 1L;
public HealthWorkerCustomSerializer() {
this(null);
}
public HealthWorkerCustomSerializer(Class clazz) {
super(clazz);
}
@Override
public void serialize(HealthWorker healthWorker, JsonGenerator jsonGenerator, SerializerProvider serializer)
throws IOException {
jsonGenerator.writeStartObject();
jsonGenerator.writeNumberField("id", healthWorker.getId());
jsonGenerator.writeStringField("full_name",
healthWorker.getName());
jsonGenerator.writeStringField("qualification", healthWorker.getQualification());
jsonGenerator.writeObjectField("yearsOfExperience", healthWorker.getYearsOfExperience());
jsonGenerator.writePOJOField("dateOfJoining", healthWorker.getDateOfJoining());
jsonGenerator.writeEndObject();
}
}
Let's assume that we can retrieve healthcare worker data with a HealthWorkerService
object, which would leverage a web service to find a health worker by ID. This is how you can set up custom serializers like the ones we created above:
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(DateTime.class, new CustomJodaDateTimeSerializer());
simpleModule.addSerializer(Double.class, new DoubleToStringCustomSerializer());
simpleModule.addSerializer(HealthWorker.class, new HealthWorkerCustomSerializer());
objectMapper.registerModule(simpleModule);
HealthWorkerService healthWorkerService = new HealthWorkerService();
HealthWorker healthWorker = healthWorkerService.findHealthWorkerById(1);
String healthWorkerCustomSerializedJson = objectMapper.writeValueAsString(healthWorker);
System.out.println(healthWorkerCustomSerializedJson);
Observe how serializers are added to a module, which is then registered by the ObjectMapper
:
{
"id": 1,
"full_name": "Dr. John",
"qualification": "FCPS",
"yearsOfExperience": "5.0",
"dateOfJoining": "2022-01-02 00:28"
}
Here we can observe that the name
field is altered to full_name
, that the value of yearsOfExperience
is returned as "5.0"
which is a String value, and that the dateOfJoining
value is returned as per the defined format.
Implementing a Custom Jackson Deserializer
The following implementation of a custom deserializer appends a value to the name
:
public class HealthWorkerCustomDeserializer extends StdDeserializer {
private static final long serialVersionUID = 1L;
public HealthWorkerCustomDeserializer() {
this(null);
}
public HealthWorkerCustomDeserializer(Class clazz) {
super(clazz);
}
@Override
public HealthWorker deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
HealthWorker healthWorker = new HealthWorker();
JsonNode jsonNode = jsonParser.getCodec().readTree(jsonParser);
JsonNode customNameNode = jsonNode.get("name");
JsonNode customQualificationNode = jsonNode.get("qualification");
JsonNode customYearsOfExperienceNode = jsonNode.get("yearsOfExperience");
JsonNode customIdNode = jsonNode.get("yearsOfExperience");
String name = "Dr. " + customNameNode.asText();
String qualification = customQualificationNode.asText();
Double experience = customYearsOfExperienceNode.asDouble();
int id = customIdNode.asInt();
healthWorker.setName(name);
healthWorker.setQualification(qualification);
healthWorker.setYearsOfExperience(experience);
healthWorker.setId(id);
return healthWorker;
}
}
Adding a deserializer is similar to adding a serializer, they are added to modules which then gets registered to the ObjectMapper
instance:
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addDeserializer(HealthWorker.class, new HealthWorkerCustomDeserializer());
objectMapper.registerModule(simpleModule);
String healthWorkerJSON = "{\n\t\"id\": 1,\n\t\"name\": \"Reham Muzzamil\",\n\t\"qualification\": \"MBBS\",\n\t\"yearsOfExperience\": 1.5\n}";
HealthWorker healthWorker = objectMapper.readValue(healthWorkerJSON,HealthWorker.class);
System.out.println(healthWorker.getName());
Running this code will produce this output:
Dr. Reham Muzzamil
As we can see from the output, Dr.
is appended to the name of the Health Worker as per the custom deserialization logic.
Conclusion
This brings us to the conclusion of the guide. We've covered the ObjectMapper class - the central API of Jackson for serialization and deserialization of Java Objects and JSON data.
We've first taken a look at how to install Jackson, and then dived into converting JSON to Java Objects - from strings, files, HTTP Responses, InputStreams and byte arrays. Then we explored conversion of JSON to Java lists and maps.
We've covered the @JsonProperty
and @JsonAlias
annotations to "bridge" mismatching field names, before converting Java Objects into JSON data.
When you don't know the structure of incoming JSON upfront - you can use the generic JsonNode
class to hold the results!
With the general usage out of the way, we've explored some of the customization flags, which modify ObjectMapper's behavior, and even implemented several serializers and deserializers of our own.