Download Definitive Guide to Jackson ObjectMapper - Serialize and Deserialize Java Objects - Java
Categories:Viewed: 78 - Published at: 6 months ago

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 defauly 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:

  1. Plain Old Java Objects (POJOs)
  2. 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 and ObjectWriter classes.
  • JsonParser and JsonGenerator 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 the readValue() 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)

</t></t>

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.
        <h4 id="jsonaliasandjsonproperty">@JsonAlias and @JsonProperty</h4>

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)

</t></t>

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)

</t></t>

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);

</path-to-file>
Note: This works the same if you use a FileReader object instead of a File object.
        <h4 id="convertjsontojavaobjectpojofromhttpresponseurl">Convert JSON to Java Object (POJO) from HTTP Response/URL</h4>

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)

</t></t>

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:

Output of the mapping of JSON data from URL to Java Object

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)

</t></t>

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);

</path-to-file>

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.

Flow diagram of Serialization and Deserialization with the Blob data type

The readValue() method of the ObjectMapper class can also be used to read byte arrays:

<t> T readValue(byte[] src, Class<t> valueType)

</t></t>

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)

</t></t>

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[]>(){});

</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>(){});

</list<healthworker></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)

</t></t>

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);

</string,></hashmap></string,>

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:

{
  "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)

</t>

As well as:

<t> T convertValue(Object fromValue, Class<t> toValueType)

</t></t>

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)

</t></t>

And:

<t> T convertValue(Object fromValue, Class<t> toValueType)

</t></t>

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 to false 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).
        <h4 id="fail_on_null_for_primitives">FAIL_ON_NULL_FOR_PRIMITIVES</h4>

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:

DTO conversion in early era

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));
    }
}

</datetime></datetime>

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());
    }
}

</double>

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();
    }
}

</healthworker>

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.

Reference: stackabuse.com

TAGS :