Jackson – Custom Serializer and Deserializer

Learn to create custom Jackson serializers and deserializers, register using the SimpleModule as well as @JsonSerialize and @JsonDeserialize.

json editor

Jackson converts a Java object to JSON (serialization) and JSON back to a Java object (deserialization) with default rules. A custom serializer and a custom deserializer replace these default rules for one class, so we control the JSON format. In Jackson, we write them by extending the classes StdSerializer and StdDeserializer, and we use them when the JSON must follow a format that the default rules cannot produce.

1. Setup

Add the latest version of Jackson to the project, if it is not there yet.

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

The demo uses the Record and ArchiveStatus classes. Lombok generates the getters, setters, constructors and the method toString(), which reduces the boilerplate code.

@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 writes every field of the object with the default value format. For example, it serializes an instance of the Record class into the following format.

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

We want a different format. The timestamp field must appear as received, and the status must be inline instead of a nested JSON object.

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

3. Creating Custom Serializer

A custom serializer extends the class StdSerializer. Jackson calls the method serialize() of our RecordSerializer every time it serializes a Record instance.

The serializer writes the JSON structure one field at a time, so we can change any field in the code. The class DateTimeFormatter is thread-safe, so the serializer can reuse one instance in 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();
  }
}

When we serialize the Record instance now, we get the 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

A custom deserializer extends the class StdDeserializer. Its method deserialize() receives the parsed JSON as a JsonNode instance, so we read the required 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);
  }
}

The deserializer now reads the custom JSON into the Record instance.

@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

Jackson needs to know about the serializer and deserializer classes before it uses them. There are two ways to register them.

4.1. Using SimpleModule

The class SimpleModule registers serializers and deserializers. It also registers bean serializer and deserializer modifiers, subtypes and mix-ins.

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

The annotations @JsonSerialize and @JsonDeserialize register the custom handlers on the class itself.

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

With the annotations, we do not register the classes with a SimpleModule.

5. Conclusion

A custom Jackson serializer extends StdSerializer and a custom deserializer extends StdDeserializer. We register them either with a SimpleModule or with the annotations @JsonSerialize and @JsonDeserialize.

Happy Learning !!

Sourcecode on Github

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.