1. Overview

In this quick tutorial, we'll show two different ways of deserializing immutable Java objects with the Jackson JSON processing library.

2. Why Do We Use Immutable Objects?

An immutable object is an object that keeps its state intact since the very moment of its creation. It means that no matter which methods of the object the end user calls, the object behaves the same way.

Immutable objects come in handy when we design a system that must work in a multithreaded environment, as immutability generally guarantees thread safety.

On the other hand, immutable objects are useful when we need to handle input from external sources. For instance, it can be user input or some data from storage. In that case, it may be critical to preserve the received data and protect it from accidental or unintended changes.

Let’s see how we can deserialize an immutable object.

3. Public Constructor

Let's consider the Employee class structure. It has two required fields: id and name, thus we define a public all-arguments constructor that has a set of arguments that matches the set of object’s fields:

public class Employee {

    private final long id;
    private final String name;

    public Employee(long id, String name) {
        this.id = id;
        this.name = name;
    }

    // getters
}

This way, we’ll have all the object’s fields initialized at the moment of creation. Final modifiers in fields’ declaration won’t let us change their values in future. To make this object deserializable, we simply need to add a couple of annotations to this constructor:

@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
public Employee(@JsonProperty("id") long id, @JsonProperty("name") String name) {
    this.id = id;
    this.name = name;
}

Let’s take a closer look at the annotations we have just added.

First of all, @JsonCreator tells Jackson deserializer to use the designated constructor for deserialization.

There are two modes that can be used as a parameter for this annotation – PROPERTIES and DELEGATING.

PROPERTIES is the most suitable when we declare an all-arguments constructor, while DELEGATING may be useful for single-argument constructors.

After that, we need to annotate each of constructor arguments with @JsonProperty stating the name of the respective property as the annotation value. We should be very careful at this step, as all the property names must match with the ones that we used during serialization.

Let's take a look at a simple unit test that covers the deserialization of an Employee object:

String json = "{\"name\":\"Frank\",\"id\":5000}";
Employee employee = new ObjectMapper().readValue(json, Employee.class);

assertEquals("Frank", employee.getName());
assertEquals(5000, employee.getId());

4. Private Constructor and a Builder

Sometimes it happens that an object has a set of optional fields. Let’s consider another class structure, Person, which has an optional age field:

public class Person {
    private final String name;
    private final Integer age;

    // getters
}

When we have a significant number of such fields, creating a public constructor may become cumbersome. In other words, we’ll need to declare a lot of arguments for the constructor and annotate each of them with @JsonProperty annotations. As a result, many repetitive declarations will make our code bloated and hard to read.

This is the case when a classical Builder pattern comes to the rescue. Let’s see how we can employ its power in deserialization. First of all, let’s declare a private all-arguments constructor and a Builder class:

private Person(String name, Integer age) {
    this.name = name;
    this.age = age;
}

static class Builder {
    String name;
    Integer age;
    
    Builder withName(String name) {
        this.name = name;
        return this;
    }
    
    Builder withAge(Integer age) {
        this.age = age;
        return this;
    }
    
    public Person build() {
        return new Person(name, age);
    } 
}

To make the Jackson deserializer use this Builder, we just need to add two annotations to our code. First of all, we need to mark our class with @JsonDeserialize annotation, passing a builder parameter with a fully qualified domain name of a builder class.

After that, we need to annotate the builder class itself as @JsonPOJOBuilder:

@JsonDeserialize(builder = Person.Builder.class)
public class Person {
    //...
    
    @JsonPOJOBuilder
    static class Builder {
        //...
    }
}

Note, that we can customize names of methods used during the build.

Parameter buildMethodName defaults to “build” and stands for the name of the method that we call when the builder is ready to generate a new object.

Another parameter, withPrefix, stands for the prefix that we add to builder methods responsible for setting properties. The default value for this parameter is “with”. That’s why we didn’t specify any of these parameters in the example.

Let's take a look at a simple unit test that covers the deserialization of a Person object:

String json = "{\"name\":\"Frank\",\"age\":50}";
Person person = new ObjectMapper().readValue(json, Person.class);

assertEquals("Frank", person.getName());
assertEquals(50, person.getAge().intValue());

5. Conclusion

In this short article, we've seen how to deserialize immutable objects using the Jackson library.

All the code related to this article can be found over on GitHub.