How to Write a Custom Serializer with Jackson

Jackson is a great framework for translating Java to JSON. It comes packaged with a number of features that make it easy to turn a Plain Old Java Object (POJO) into JSON with little effort. However, sometimes more complex translation is necessary and the out-of-the-box features don’t cut it. Fortunately, Jackson provides a way to write custom code and control how any object is translated to JSON.

One great use case of a custom serializer is for overriding default behavior. For example, the Interval class is serialized as a dash-separated string by default. An interval from 1-10-2010 to 9-25-2015 would serialize like this:

"1263963600000-1443153600000"

That is pretty terrible to read as a human, and I definitely do not want to force all clients of my API to parse that string. Instead, I want to return a start and end property with their respective values.

{
  "start" : "01-10-2010",
  "end" : "09-25-2015"
}

This can be accomplished by writing a custom serializer for Interval, which involves the following steps:

  1. Implement a serializer class by extending JsonSerializer.
  2. Create a module to bundle the custom functionality.
  3. Register the module with ObjectMapper.
  4. Write a test to confirm the new JSON format.

I’ll walk you through this process below.

Implement a Serializer by Extending JsonSerializer

The first step in creating a custom serializer is to extend Jackson’s JsonSerializer. This abstract class includes a method called serialize, which is where logic for building a custom JSON representation belongs.

Notice that three arguments are injected into serialize:

  • value: This is the actual value that is being serialized. Every time an Interval is serialized, this method will be called, and the value will be passed along to it. Since an instance of the object being serialized is available, it allows any of the properties to be accessed and customized before being sent to the JSON generator.
  • JsonGenerator: This Jackson utility is responsible for writing JSON. It can do things like write fields or values and start new JSON objects or arrays.
  • SerializerProvider: This provider will not be useful for the current example, but it is used for getting access to other serializers and configurations registered with the ObjectMapper.

Now that there is a class for the custom serializer, we need to provide instructions for how to render JSON. On a high level, the instructions should like this:

  1. Write the start of a JSON object ({).
  2. Write a start field ("start" : "...").
  3. Write an end field ("end" : "...").
  4. Write the end of a JSON object (}).

Since the JsonGenerator is responsible for writing JSON, the instructions above can be translated to the following code:


public class IntervalSerializer extends JsonSerializer {
  private static final DateTimeFormatter DATE_FORMATTER = 
          ISODateTimeFormat.date();

  @Override
  public void serialize(Interval interval,
                        JsonGenerator jGen,
                        SerializerProvider serializerProvider) {
    jGen.writeStartObject();
    jGen.writeStringField("start", interval.getStart());
    jGen.writeStringField("end", interval.getEnd());
    jGen.writeEndObject();
  }
}

Create a Module to Bundle the Custom Functionality

Now that the custom serializer has been built, we need a module for bundling the functionality. Modules are the preferred Jackson pattern for organizing serializers and deserializers, and they can be registered with the ObjectMapper.

Creating a module is straightforward, and for this example, it can be as simple as:


public class IntervalModule extends SimpleModule {
  private static final String NAME = "CustomIntervalModule";
  private static final VersionUtil VERSION_UTIL = new VersionUtil() {};

  public IntervalModule() {
    super(NAME, VERSION_UTIL.version());
    addSerializer(Interval.class, new IntervalSerializer());
  }
}

There are a few things to note in the above code:

  • The custom module extends SimpleModule. This very basic implementation that Jackson provides is sufficient for most cases. I personally have never needed to use something different.
  • super is called with a name and a version. The name argument is useful for detecting when a module is registered multiple times in order to avoid collisions. VersionUtil is a handy Jackson utility that will attempt to intelligently version the module.
  • The custom serializer is registered using addSerializer.

Register the Module with ObjectMapper

The ObjectMapper is one of the most important aspects of Jackson. It is the center of configuration and is responsible for all data binding. In order to activate the custom functionality, the newly created module must be registered with the ObjectMapper.

For most projects, there should be one global ObjectMapper that is instantiated during bootstrap and contains all of the custom configuration for the application. I recommend managing this by creating a custom extension, and registering any custom modules like this:


public class CustomObjectMapper extends ObjectMapper {
  public CustomObjectMapper() {
    registerModule(new IntervalModule());
    enable(SerializationFeature.INDENT_OUTPUT);
  }
}

Write a Test to Confirm the New JSON Format

Since no feature is complete without adequate testing, let’s write a test to confirm our custom serialization. If the expected JSON is saved to a file called interval.json, then a simple test can verify the behavior of the new serializer:


public class IntervalSerializerTest {
  private static final CustomObjectMapper OBJECT_MAPPER = 
          new CustomObjectMapper();

  @Test
  public void testIntervalSerialization() throws Exception {
    DateTime startDate = DateTime.parse("2010-01-20");
    DateTime endDate = DateTime.parse("2015-09-25");
    Interval interval = new Interval(startDate, endDate);

    String result = OBJECT_MAPPER.writeValueAsString(interval);
    String expected = new String(readAllBytes(get("interval.json")));

    assertEquals(expected, result);
  }
}

And with a green passing test, we have successfully implemented a custom serializer.