Jackson – Custom Serializer and Deserializer

Learn to create a custom serializer and custom deserializer for controlling the JSON to POJO conversion and vice versa using Jackson‘s StdSerializer and StdDeserializer classes.

1. Setup

Add the latest version of Jackson, if you have not already added it to the project.

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>${jackson.version}</version>
</dependency>

For demo purposes, we will use the following Record and ArchiveStatus classes. We are using Lombok to reduce the boilerplate code such as getters, setters, constructors and toString() method.

@lombok.Data
@AllArgsConstructor
@NoArgsConstructor
class Record {

  private Long id;
  private String message;
  private ZonedDateTime timestamp;
  private ArchiveStatus status;
}

@lombok.Data
@AllArgsConstructor
@NoArgsConstructor
class ArchiveStatus {

  private Boolean active;
}

2. Default Serialization and The Requirement

By default, Jackson produces the JSON consisting of default fields structure and default value formats. For example, an instance of Record class will be serialized into the following format.

{
  "id" : 1,
  "message" : "test-message",
  "timestamp" : 1640998860.000000000,
  "status" : {
    "active" : true
  }
}

We want to customize the format to a custom format. The timestamp field should be used for display as received, and the status should be inline instead of a nested JSON field.

{
	"id":1,
	"message":"test-message",
	"timestamp":"2022-01-01 01:00:01 AM GMT",
	"status":"active"
}

3. Creating Custom Serializer

Let us create a custom serializer according to our needs by extending StdSerializer class. The RecordSerializer‘s serialize() method will be invoked everytime Jackson needs to serialize its instance.

This class will create our desired JSON structure one field at a time. You can further customize the following code according to your needs. Note that DateTimeFormatter is a thread-safe object so we can reuse it with different threads.

class RecordSerializer extends StdSerializer<Record> {

  private static DateTimeFormatter dtf
      = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:ss:mm a z", Locale.US);

  protected RecordSerializer() {
    super(Record.class);
  }

  @Override
  public void serialize(Record value, JsonGenerator gen,
                        SerializerProvider serializers) throws IOException {

    gen.writeStartObject();
    gen.writeNumberField("id", value.getId());
    gen.writeStringField("message", value.getMessage());
    gen.writeStringField("timestamp", dtf.format(value.getTimestamp()));
    if (value.getStatus() != null) {
      gen.writeStringField("status", value.getStatus().getActive() ?
          "active" : "inactive");
    }
    gen.writeEndObject();
  }
}

Now, if we serialize the Record instance then we will get the output JSON in the desired format.

@Test
void testCustomSerialization() throws JsonProcessingException {

  ZonedDateTime zdt = ZonedDateTime.of(LocalDateTime.of(2022, 1, 1, 1, 1), ZoneId.of("GMT"));
  Record record = new Record(1L, "test-message", zdt, new ArchiveStatus(true));

  JsonMapper jsonMapper = new JsonMapper();

  String json = jsonMapper.writerWithDefaultPrettyPrinter()
      .writeValueAsString(record);

  Assertions.assertEquals("{\"id\":1,\"message\":\"test-message\"," +
      "\"timestamp\":\"2022-01-01 01:00:01 AM GMT\",\"status\":\"active\"}" , json);
}

4. Custom Deserialization

The custom deserializer is created by extending the StdDeserializer class. Its deserialize() method receives the parsed JSON as a JsonNode instance. We need to fetch the specific JSON fields from the JsonNode and build the Record instance.

class RecordDeserializer extends StdDeserializer<Record> {

  private static DateTimeFormatter dtf 
  		= DateTimeFormatter.ofPattern("yyyy-MM-dd hh:ss:mm a z", Locale.US);

  public RecordDeserializer() {
    this(null);
  }

  public RecordDeserializer(Class<?> vc) {
    super(vc);
  }

  @Override
  public Record deserialize(JsonParser parser, DeserializationContext ctx)
      throws IOException, JacksonException {

    JsonNode node = parser.getCodec().readTree(parser);
    Integer id = (Integer) ((IntNode) node.get("id")).numberValue();
    String message = node.get("message").asText();
    String timestamp = node.get("timestamp").asText();
    ArchiveStatus status = new ArchiveStatus(false);

    if(node.get("status") != null) {
      String active = node.get("status").asText();
      if("active".equalsIgnoreCase(active)) {
        status.setActive(true);
      }
    }

    return new Record(id.longValue(), message, ZonedDateTime.parse(timestamp, dtf), status);
  }
}

Now we can deserialize the custom JSON into the Record instance, as expected.

@Test
void testCustomDeserialization() throws JsonProcessingException {
  ZonedDateTime zdt = ZonedDateTime
      .of(LocalDateTime.of(2022, 1, 1, 1, 1), ZoneId.of("GMT"));

  String json = "{\"id\":1,\"message\":\"test-message\"," +
      "\"timestamp\":\"2022-01-01 01:00:01 AM GMT\",\"status\":\"active\"}";

  JsonMapper jsonMapper = new JsonMapper();
  Record record = jsonMapper.readValue(json, Record.class);

  Assertions.assertEquals(1L, record.getId());
  Assertions.assertEquals("test-message", record.getMessage());
  Assertions.assertEquals(zdt, record.getTimestamp());
  Assertions.assertEquals(true, record.getStatus().getActive());
}

4. Registering Custom Serializer and Deserializer

Let’s check out the different ways to register the above create serializer and deserializer classes with Jackson runtime.

4.1. Using SimpleModule

The SimpleModule class registration of serializers and deserializers, bean serializer and deserializer modifiers, registration of subtypes and mix-ins, and some other commonly needed aspects.

JsonMapper jsonMapper = new JsonMapper();

SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Record.class, new RecordSerializer());
simpleModule.addDeserializer(Record.class, new RecordDeserializer());

jsonMapper.registerModule(simpleModule);

4.2. Using @JsonSerialize and @JsonDeserialize

Another efficient way to register the custom handlers is using the @JsonSerialize and @JsonDeserialize annotations as follows:

@JsonSerialize(using = RecordSerializer.class)
@JsonDeserialize(using = RecordDeserializer.class)
class Record {
	...
}

When using annotations, we do not need to add the classes using SimpleModule registration process.

5. Conclusion

In this Jackson tutorial, we learned to create custom Jackson serializers and deserializers. We also learned to register the custom handlers using the SimpleModule as well as @JsonSerialize and @JsonDeserialize annotations.

Happy Learning !!

Sourcecode on Github

Comments

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.