Jackson serializes java.util.Date as a numeric timestamp and the other date-time types as numbers by default. To change the format, we set a date pattern on the JsonMapper or register a custom serializer and deserializer. We customize the format when a client or a database expects a readable pattern instead of numbers. The sections below cover the default behavior, custom patterns and custom handlers.
1. Setup
Add the latest version of Jackson to the project, if it is not there yet.
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.13.3</version>
</dependency>
For demo purposes, we will use the following Message class.
@JsonInclude(JsonInclude.Include.NON_NULL)
class Message {
private Long id;
private String text;
private Date date;
private LocalDateTime localDateTime;
//constructors, getters, setters
}
2. Default Serialization and Deserialization
By default, Jackson serializes java.util.Date and other date-time types such as java.time.LocalDateTime in numeric formats. It converts the Date to a long and the other types to strings made of numbers.
To support the Java 8 date-time classes, we register the JavaTimeModule with the JsonMapper or ObjectMapper.
Let us check the serialization with the default settings.
@Test
void testDefaultSerialization_ThenSuccess() throws JsonProcessingException {
Message message = new Message(1L, "test");
message.setDate(new Date(1661518473905L));
message.setLocalDateTime(LocalDateTime.of(2022,8,30,14,16,20,0));
JsonMapper jsonMapper = new JsonMapper();
jsonMapper.registerModule(new JavaTimeModule());
String json = jsonMapper.writeValueAsString(message);
Assertions.assertEquals("{\"id\":1,\"text\":\"test\"," +
"\"date\":1661518473905,\"localDateTime\":[2022,8,30,14,16,20]}", json);
}
Deserialization from JSON to the POJO reads the same numeric format.
@Test
void testDefaultDeserialization_ThenSuccess() throws JsonProcessingException {
String json = "{\"id\":1,\"text\":\"test\",\"date\":1661518473905," +
"\"localDateTime\":[2022,8,30,14,16,20]}";
JsonMapper jsonMapper = new JsonMapper();
jsonMapper.registerModule(new JavaTimeModule());
Message message = jsonMapper.readValue(json, Message.class);
Assertions.assertEquals(1661518473905L, message.getDate().getTime());
Assertions.assertEquals(LocalDateTime.of(2022,8,30,14,16,20,0), message.getLocalDateTime());
}
3. Using Custom Date Time Patterns
3.1. Serialization
To serialize date-time types with a custom pattern, we set a SimpleDateFormat on the JsonMapper.
jsonMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
The configured JsonMapper now writes the supported types in the custom pattern.
@Test
void testCustomFormat_ThenSuccess() throws JsonProcessingException {
Message message = new Message(1L, "test");
message.setDate(new Date(1661518473905L));
message.setLocalDateTime(LocalDateTime.of(2022, 1, 1, 15, 30));
JsonMapper jsonMapper = new JsonMapper();
jsonMapper.registerModule(new JavaTimeModule());
jsonMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
String json = jsonMapper.writeValueAsString(message);
Assertions.assertEquals("{\"id\":1,\"text\":\"test\"," +
"\"date\":\"2022-08-26 18:24:33\"," +
"\"localDateTime\":\"2022-01-01T15:30:00\"}", json);
}
Not every Java type supports every custom pattern. For example, the LocalDate class has no time information, so the pattern in the code above does not give the desired result for it.
3.2. Deserialization
Deserialization is very similar to serialization, and it reads the dates with the custom pattern configured through the SimpleDateFormat class.
@Test
void testCustomDateFormatDeserialization_ThenSuccess() throws JsonProcessingException {
String json = "{\"id\":1,\"text\":\"test\",\"date\":1661518473905," +
"\"localDateTime\":[2022,8,30,14,16,20]}";
JsonMapper jsonMapper = new JsonMapper();
jsonMapper.registerModule(new JavaTimeModule());
jsonMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
Message message = jsonMapper.readValue(json, Message.class);
Assertions.assertEquals(1661518473905L, message.getDate().getTime());
Assertions.assertEquals("2022-08-30T14:16:20", message.getLocalDateTime().toString());
}
4. Custom Serializer and Deserializer
To control how Jackson converts JSON to a POJO and back, we write custom classes and register them with Jackson.
4.1. Custom Serializer
The custom serializer for the LocalDateTime type extends the class StdSerializer and overrides its method serialize().
The serializer applies any custom pattern through the Java DateTimeFormatter class, so it can follow complex format requirements.
class CustomLocalDateTimeSerializer
extends StdSerializer<LocalDateTime> {
private static DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public CustomLocalDateTimeSerializer() {
this(null);
}
public CustomLocalDateTimeSerializer(Class<LocalDateTime> t) {
super(t);
}
@Override
public void serialize(
LocalDateTime value,
JsonGenerator gen,
SerializerProvider arg2)
throws IOException, JsonProcessingException {
gen.writeString(formatter.format(value));
}
}
This serializer replaces the Jackson support for LocalDateTime, so we skip the JavaTimeModule. Instead, we register CustomLocalDateTimeSerializer with a SimpleModule.
@Test
void testCustomSerializer_ThenSuccess() throws JsonProcessingException {
Message message = new Message(1L, "test");
message.setLocalDateTime(LocalDateTime.of(2022, 1, 1, 15, 30));
JsonMapper jsonMapper = new JsonMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(LocalDateTime.class, new CustomLocalDateTimeSerializer());
jsonMapper.registerModule(module);
String json = jsonMapper.writeValueAsString(message);
Assertions.assertEquals("{\"id\":1,\"text\":\"test\"," +
"\"localDateTime\":\"2022-01-01 15:30:00\"}", json);
}
4.2. Custom Deserializer
The custom date deserializer follows the same pattern as the serializer, and it extends the class StdDeserializer.
class CustomLocalDateTimeDeserializer extends StdDeserializer<LocalDateTime> {
private DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
public CustomLocalDateTimeDeserializer() {
this(null);
}
public CustomLocalDateTimeDeserializer(Class<?> vc) {
super(vc);
}
@Override
public LocalDateTime deserialize(JsonParser jsonparser,
DeserializationContext context)
throws IOException {
String date = jsonparser.getText();
return LocalDateTime.parse(date, formatter);
}
}
Let us check the custom deserialization with a simple test.
@Test
void testCustomLocalDateTimeDeserializer_ThenSuccess() throws JsonProcessingException {
String json = "{\"id\":1,\"text\":\"test\",\"localDateTime\":\"1986-04-08 12:30\"}";
JsonMapper jsonMapper = new JsonMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(LocalDateTime.class, new CustomLocalDateTimeDeserializer());
jsonMapper.registerModule(module);
Message message = jsonMapper.readValue(json, Message.class);
Assertions.assertEquals("1986-04-08T12:30", message.getLocalDateTime().toString());
}
5. Conclusion
In this tutorial, we saw that Jackson serializes dates in numeric formats by default. We also set custom patterns with SimpleDateFormat and wrote custom serializer and deserializer classes.
Happy Learning !!