Using Optional with Jackson

1. Introduction

In this article, we’ll give an overview of the Optional class, and then explain some problems that we might run into when using it with Jackson.

Following this, we’ll introduce a solution which will get Jackson to treat Optionals as if they were ordinary nullable objects.

2. Problem Overview

First, let’s take a look at what happens when we try to serialize and deserialize Optionals with Jackson.

2.1. Maven Dependency

To use Jackson, let’s make sure we’re using its latest version:

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

2.2. Our Book Object

Then, let’s create a class Book, containing one ordinary and one Optional field:

public class Book {
   String title;
   Optional<String> subTitle;
   
   // getters and setters omitted
}

Keep in mind that Optionals should not be used as fields and we are doing this to illustrate the problem.

2.3. Serialization

Now, let’s instantiate a Book:

Book book = new Book();
book.setTitle("Oliver Twist");
book.setSubTitle(Optional.of("The Parish Boy's Progress"));

And finally, let’s try serializing it using a Jackson ObjectMapper:

String result = mapper.writeValueAsString(book);

We’ll see that the output of the Optional field, does not contain its value, but instead a nested JSON object with a field called present:

{"title":"Oliver Twist","subTitle":{"present":true}}

Although this may look strange, it’s actually what we should expect.

In this case, isPresent() is a public getter on the Optional class. This means it will be serialized with a value of true or false, depending on whether it is empty or not. This is Jackson’s default serialization behavior.

If we think about it, what we want is for actual the value of the subtitle field to be serialized.

2.4. Deserialization

Now, let’s reverse our previous example, this time trying to deserialize an object into an Optional. We’ll see that now we get a JsonMappingException:

@Test(expected = JsonMappingException.class)
public void givenFieldWithValue_whenDeserializing_thenThrowException
    String bookJson = "{ \"title\": \"Oliver Twist\", \"subTitle\": \"foo\" }";
    Book result = mapper.readValue(bookJson, Book.class);
}

Let’s view the stack trace:

com.fasterxml.jackson.databind.JsonMappingException:
  Can not construct instance of java.util.Optional:
  no String-argument constructor/factory method to deserialize from String value ('The Parish Boy's Progress')

This behavior again makes sense. Essentially, Jackson needs a constructor which can take the value of subtitle as an argument. This is not the case with our Optional field.

3. Solution

What we want, is for Jackson to treat an empty Optional as null, and to treat a present Optional as a field representing its value.

Fortunately, this problem has been solved for us. Jackson has a set of modules that deal with JDK 8 datatypes, including Optional.

3.1. Maven Dependency and Registration

First, let’s add the latest version as a Maven dependency:

<dependency>
   <groupId>com.fasterxml.jackson.datatype</groupId>
   <artifactId>jackson-datatype-jdk8</artifactId>
   <version>2.9.6</version>
</dependency>

Now, all we need to do is register the module with our ObjectMapper:

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new Jdk8Module());

3.2. Serialization

Now, let’s test it. If we try and serialize our Book object again, we’ll see that there is now a subtitle, as opposed to a nested JSON:

Book book = new Book();
book.setTitle("Oliver Twist");
book.setSubTitle(Optional.of("The Parish Boy's Progress"));
String serializedBook = mapper.writeValueAsString(book);
 
assertThat(from(serializedBook).getString("subTitle"))
  .isEqualTo("The Parish Boy's Progress");

If we try serializing an empty book, it will be stored as null:

book.setSubTitle(Optional.empty());
String serializedBook = mapper.writeValueAsString(book);
 
assertThat(from(serializedBook).getString("subTitle")).isNull();

3.3. Deserialization

Now, let’s repeat our tests for deserialization. If we reread our Book, we’ll see that we no longer get a JsonMappingException:

Book newBook = mapper.readValue(result, Book.class);
 
assertThat(newBook.getSubTitle()).isEqualTo(Optional.of("The Parish Boy's Progress"));

Finally, let’s repeat the test again, this time with null. We’ll see that yet again we don’t get a JsonMappingException, and in fact, have an empty Optional:

assertThat(newBook.getSubTitle()).isEqualTo(Optional.empty());

4. Conclusion

We’ve shown how to get around this problem by leveraging the JDK 8 DataTypes module, demonstrating how it enables Jackson to treat an empty Optional as null, and a present Optional as an ordinary field.

The implementation of these examples can be found over on GitHub; this is a Maven-based project, so should be easy to run as is.