Jackson Serialize and Deserialize LocalDate Example
1. Introduction
Creating a custom serializer and deserializer via the Jackson library is a common task in Java applications. The LocalDate is an immutable date-time object that represents a date, often viewed as year-month-day. In this example, I will demonstrate how to serialize and deserialize in the following three ways.
- Register the
JavaTimeModulealong with the@JsonFormatannotation. - Register a
CustLocalDateModulewith the custom serializer and deserializer. - Register a
SimpleModulewith desired serializer and/or deserializer.
2. Setup a Maven Project
In this step, I will set up a maven project with Junit, Jackson, and Lombok libraries. The Lombok is added to reduce the boilerplate code.
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.zheng.demo</groupId> <artifactId>jackson-localdate</artifactId> <version>0.0.1-SNAPSHOT</version> <dependencies> <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.17.1</version> </dependency> <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.17.1</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> <version>2.17.1</version> <!-- Use the latest version available --> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.10.2</version> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.32</version> <scope>provided</scope> </dependency> </dependencies> </project>
3. Person
In this step, I will create a Person.java class which has four data members. Note: the Lombok annotations are used to reduce the boilerplate code.
age– an optional integer value.name-a non-null string value.localDateObj– aLocalDateobject without any annotation.localDateJsr310– aLocalDateobject annotated with@JsonFormatwithLocalDateSerializerandLocalDateDeserializer.
Person.java
package org.zheng.demo.data;
import java.time.LocalDate;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
@Data
@NoArgsConstructor
@RequiredArgsConstructor
public class Person {
private int age;
@NonNull
private String name;
private LocalDate localDateObj;
@JsonDeserialize(using = LocalDateDeserializer.class)
@JsonSerialize(using = LocalDateSerializer.class)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "E dd-MMM-yyyy G")
private LocalDate localDateJsr310;
}
- Line 26: the
LocalDateObjhas no any annotation, theobjectmapper'sregistered or default module will be used to serialize and deserialize. - Line 28-31: the
LocalDateJsr310annotated with JSR310@JsonFormat,LocalDateDeserializer, andLocalDateSerializer. It will be rendered by the JSR310JavaTimeModule.
Please note, the pattern string is supported with SimpleDateFormat. Here are the common symbols used in the patterns:
- y: Year (e.g., yy for 2-digit year, yyyy for 4-digit year)
- d: Day of month (e.g., dd for 2-digit day)
- M: Month (e.g., MM for 2-digit month, MMM for abbreviated month, MMMM for full month name)
- E: Day of week (e.g., E for abbreviated day name, EEEE for full day name)
3.1 TestDefaultObjectMapper
In this step, I will create a Junit TestDefaultObjectMapper.java which includes two tests.
test_default_ok_without_loaldate– the defaultobjectMapperis ok to serialize and deserialize when the object has no value for theLocalDatefields.test_default_throw_exception_with_localDate– the defaultobjectMapperthrows an exception when serializing or deserializing an object with aLocalDatefield populated.
TestDefaultObjectMapper.java
package org.zheng.demo;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.LocalDate;
import org.junit.jupiter.api.Test;
import org.zheng.demo.data.Person;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
class TestDefaultObjectMapper {
ObjectMapper ob = new ObjectMapper();
Person per = new Person("Zheng");
@Test
void test_default_ok_without_loaldate() {
try {
String jsonStr = ob.writeValueAsString(per);
System.out.println(jsonStr);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
@Test
void test_default_throw_exception_with_localDate() {
per.setLocalDateObj(LocalDate.of(1970, 01, 01));
InvalidDefinitionException expectedException = assertThrows(InvalidDefinitionException.class,
() -> ob.writeValueAsString(per));
assertTrue(expectedException.getMessage().contains(
"Java 8 date/time type `java.time.LocalDate` not supported by default: add Module \"com.fasterxml.jackson.datatype:jackson-datatype-jsr310\" to enable handling"));
}
}
- Line 32: set the
Personobject with aLocalDatevalue. - Line 34: verify an exception is thrown by the default
objectMapper.
Run the test and capture the output:
TestDefaultObjectMapper output
{"age":0,"name":"Zheng","localDateObj":null,"localDateJsr310":null}
- If the Java pojo does not have any
LocalDatefields, then the defaultobjectMapperworks fine. otherwise, it will throw an exception.
4. Test LocalDate with Jackson JavaTimeModule
Jackson library provides the JavaTimeModule which can be registered by objectMapper.
4.1 Test JavaTimeModule
In this step, I will create a TestJavaTimeModule.java class which verifies the LocalDate.
TestJavaTimeModule.java
package org.zheng.demo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.LocalDate;
import org.junit.jupiter.api.Test;
import org.zheng.demo.data.Person;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
class TestJavaTimeModule {
@Test
void test_enable_objectMapper_jsr310() {
ObjectMapper ob = new ObjectMapper();
ob.registerModule(new JavaTimeModule());
Person per = new Person("Zheng");
per.setLocalDateJsr310(LocalDate.of(2020, 01, 01));
per.setLocalDateObj(LocalDate.of(1970, 01, 01));
try {
String jsonString = ob.writeValueAsString(per);
assertEquals(
"{\"age\":0,\"name\":\"Zheng\",\"localDateObj\":[1970,1,1],\"localDateJsr310\":\"Wed 01-Jan-2020 AD\"}",
jsonString);
System.out.println(ob.writerWithDefaultPrettyPrinter().writeValueAsString(per));
Person readPer = ob.readValue(jsonString, Person.class);
assertTrue(per.equals(readPer));
assertEquals("Zheng", readPer.getName());
assertEquals(0, readPer.getAge());
assertEquals(LocalDate.of(1970, 1, 1), readPer.getLocalDateObj());
assertEquals(LocalDate.of(2020, 1, 1), readPer.getLocalDateJsr310());
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
- Line 20: register the
JavaTimeModulefor theobjectMapperinstance. - Line 27: serialize via
objectMapper.writeValueAsString. Note, the twoLocalDatefields have different string formats. - Line 33: deserialize via
objectMapper.readValueand theLocalDates are mapped correctly.
Execute the Junit test and capture the output.
test_enable_objectMapper_jsr310 output
{
"age" : 0,
"name" : "Zheng",
"localDateObj" : [ 1970, 1, 1 ],
"localDateJsr310" : "Wed 01-Jan-2020 AD"
}
- Line 4: the
LocalDatehas the default string format pattern. - Line 5: the
LocalDatehas the customized string pattern defined at thePersonclass at step 3.
5. Custom Serializer and Deserializer
If your application requires more customization for the LocalDate class, then you can create your own customized localDate serializer and deserializer.
5.1 Custom LocalDate Serializer
In this step, I will create a customized LocalDate serializer which adds the “Past:” to the “yyyy-MMMM-dd” if the LocalDate is in the past.
CustLocalDateSerializer.java
package org.zheng.demo.module;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
public class CustLocalDateSerializer extends JsonSerializer<LocalDate> {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MMMM-dd");
@Override
public void serialize(LocalDate date, JsonGenerator gen, SerializerProvider provider) throws IOException {
if (date.isBefore(LocalDate.now())) {
// Apply special formatting for past dates
String formattedDate = "Past: " + date.format(formatter);
gen.writeString(formattedDate);
} else {
// Default formatting
String formattedDate = date.format(formatter);
gen.writeString(formattedDate);
}
}
}
- Line 11: the custom serializer extends from
JsonSerializer<LocalDate>. - Line 16: implements the custom logic to serialize the
LocalDate.
5.2 Custom LocalDate De-Serializer
In this step, I will create a customized LocalDate deserializer which maps the date string with “Past yyyy-MMMM-dd” pattern to the LocalDate class.
CustLocalDateDeserializer.java
package org.zheng.demo.module;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
public class CustLocalDateDeserializer extends JsonDeserializer<LocalDate> {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MMMM-dd");
@Override
public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String date = p.getText();
if (date.startsWith("Past")) {
date = date.substring("Past: ".length());
}
return LocalDate.parse(date, formatter);
}
}
- Line 11: the customer deserializer extends
JsonDeserializer. - Line 16: implements the custom deserialize logic for
LocalDate.
5.3 Custom LocalDate Module
In this step, I will set up a customLocalModule which extends from SimpleModule and configure the serializer and deserializer built at step 5.1 and 5.2.
CustLocalDateModule
package org.zheng.demo.module;
import java.time.LocalDate;
import com.fasterxml.jackson.databind.module.SimpleModule;
public class CustLocalDateModule extends SimpleModule {
private static final long serialVersionUID = -5613434548817431041L;
public CustLocalDateModule() {
addSerializer(LocalDate.class, new CustLocalDateSerializer());
addDeserializer(LocalDate.class, new CustLocalDateDeserializer());
}
}
- Line 7: create a
custLocalDateModuleextends fromSimpleModule. - Line 12-13: add the serializer and deserializer.
5.4 Test Custom LocalDate Module
In this step, I will create a TestCustLocalDateModule.java which registers the CustLocalDateModule created at step 5.3.
TestCustLocalDateModule.java
package org.zheng.demo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.LocalDate;
import org.junit.jupiter.api.Test;
import org.zheng.demo.data.Person;
import org.zheng.demo.module.CustLocalDateModule;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class TestCustLocalDateModule {
@Test
void test_custLocalDate() throws JsonProcessingException {
ObjectMapper ob = new ObjectMapper();
Person per = new Person("Zheng");
ob.registerModule(new CustLocalDateModule());
per.setLocalDateJsr310(LocalDate.of(2020, 01, 01));
per.setLocalDateObj(LocalDate.of(1970, 01, 01));
String jsonString = ob.writeValueAsString(per);
System.out.println(ob.writerWithDefaultPrettyPrinter().writeValueAsString(per));
Person readPer = ob.readValue(jsonString, Person.class);
assertTrue(per.equals(readPer));
assertEquals("Zheng", readPer.getName());
assertEquals(0, readPer.getAge());
assertEquals(LocalDate.of(1970, 1, 1), readPer.getLocalDateObj());
assertEquals(LocalDate.of(2020, 1, 1), readPer.getLocalDateJsr310());
}
}
- Line 22: the
objectMapperis registed with a newCustLocalDateModuleobject.
Execute the Junit test and capture the output.
TestCustLocalDateModule output
{
"age" : 0,
"name" : "Zheng",
"localDateObj" : "Past: 1970-January-01",
"localDateJsr310" : "Wed 01-Jan-2020 AD"
}
- Line 4: the
LocalDatehas the customized string format pattern defined at step 5.1. - Line 5: the
LocalDatehas the customized string pattern defined at thePersonclass at step 3.
5.5 Test SimpleModule
In this step, I will create a TestSimpleModule.java which has three tests:
test_simpleModule_with_cust_serializer– register aSimpleModulewith bothCustLocalDateSerializerandCustLocalDateDeserializerand works as expected.test_simpleModule_with_serialize_then_deserialze_got_exception– register aSimpleModuleand only register withCustLocalDateSerializer, then it will throw an exception on deserializing aLocalDate.test_simpleModule_ok_at_null– register aSimpleModulewith onlyCustLocalDateSerializer, it’s ok as long as there is no value in theLocalDatefield.
TestSimpleModule.java
package org.zheng.demo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.LocalDate;
import org.junit.jupiter.api.Test;
import org.zheng.demo.data.Person;
import org.zheng.demo.module.CustLocalDateDeserializer;
import org.zheng.demo.module.CustLocalDateSerializer;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import com.fasterxml.jackson.databind.module.SimpleModule;
public class TestSimpleModule {
ObjectMapper ob = new ObjectMapper();
Person per = new Person("Zheng");
SimpleModule module = new SimpleModule();
@Test
void test_simpleModule_with_cust_serializer() throws JsonProcessingException {
// Registering the customized serialize and de-serialize
module.addSerializer(LocalDate.class, new CustLocalDateSerializer());
module.addDeserializer(LocalDate.class, new CustLocalDateDeserializer());
// Register the module with the ObjectMapper
ob.registerModule(module);
per.setLocalDateJsr310(LocalDate.of(2020, 01, 01));
per.setLocalDateObj(LocalDate.of(1970, 01, 01));
String jsonString = ob.writeValueAsString(per);
System.out.println(ob.writerWithDefaultPrettyPrinter().writeValueAsString(per));
Person readPer = ob.readValue(jsonString, Person.class);
assertTrue(per.equals(readPer));
assertEquals("Zheng", readPer.getName());
assertEquals(0, readPer.getAge());
assertEquals(LocalDate.of(1970, 1, 1), readPer.getLocalDateObj());
assertEquals(LocalDate.of(2020, 1, 1), readPer.getLocalDateJsr310());
}
@Test
void test_simpleModule_with_serialize_then_deserialze_got_exception() throws JsonProcessingException {
// Registering the customized serialize only
module.addSerializer(LocalDate.class, new CustLocalDateSerializer());
// Register the module with the ObjectMapper
ob.registerModule(module);
per.setLocalDateJsr310(LocalDate.of(2020, 01, 01));
per.setLocalDateObj(LocalDate.of(1970, 01, 01));
String jsonStr = ob.writeValueAsString(per);
System.out.println(ob.writerWithDefaultPrettyPrinter().writeValueAsString(per));
InvalidDefinitionException exception = assertThrows(InvalidDefinitionException.class,
() -> ob.readValue(jsonStr, Person.class));
assertTrue(exception.getMessage().contains("Java 8 date/time type `java.time.LocalDate`"));
}
@Test
void test_simpleModule_ok_at_null() throws JsonProcessingException {
// Registering the customized serialize only
module.addSerializer(LocalDate.class, new CustLocalDateSerializer());
// Register the module with the ObjectMapper
ob.registerModule(module);
per.setLocalDateJsr310(LocalDate.of(2020, 01, 01));
String jsonStr = ob.writeValueAsString(per);
System.out.println(ob.writerWithDefaultPrettyPrinter().writeValueAsString(per));
Person readPer = ob.readValue(jsonStr, Person.class);
assertEquals("Zheng", readPer.getName());
assertEquals(0, readPer.getAge());
assertEquals(LocalDate.of(2020, 1, 1), readPer.getLocalDateJsr310());
}
}
- Line 27, 28, 31: set up the
addSerializerandaddDeserializerto aSimpleModuleand register it to theobjectMapper. - Line 50, 53: only set up the
addSerializerto aSimpleModuleand register it to theobjectMapper. - Line 68, 71: only set up the
addSerializerto aSimpleModuleand register it to theobjectMapper. The difference from the second test is that it will throw an exception on the deserialization due to theSimpleModulenot adding the deserializer as the first test did.
Ran the Junit tests and captured the results as the following screenshot:
Here is the unit test’s output.
TestSimpleModule output
{
"age" : 0,
"name" : "Zheng",
"localDateObj" : "Past: 1970-January-01",
"localDateJsr310" : "Wed 01-Jan-2020 AD"
}
{
"age" : 0,
"name" : "Zheng",
"localDateObj" : "Past: 1970-January-01",
"localDateJsr310" : "Wed 01-Jan-2020 AD"
}
{
"age" : 0,
"name" : "Zheng",
"localDateObj" : null,
"localDateJsr310" : "Wed 01-Jan-2020 AD"
}
- Line 4,10,16: the
localDateObjis rendered with format defined by theobjectMapper‘s module. - Line 5,11,17: the
localDateJsr310is rendered with format defined at thePersonclass.
6. Conclusion
In this example, I showed three ways to serialize and deserialize LocalDate with Jackson libraries. The built-in JavaTimeModule can handle most of the date string patterns. I also showed how to create a custom serializer and deserializer for the LocalDate class. As you saw in the example, the annotation @JsonFormat has a higher precedence than the custom serializer.
7. Download
This was an example of a maven project which demonstrates how to serialize and deserialize from and to the LocalDate by registering a Jackson built-in JavaTimeModule or registering a customized SimpleModule.
You can download the full source code of this example here: Jackson Serialize and Deserialize LocalDate Example






