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
JavaTimeModule
along with the@JsonFormat
annotation. - Register a
CustLocalDateModule
with the custom serializer and deserializer. - Register a
SimpleModule
with 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
– aLocalDate
object without any annotation.localDateJsr310
– aLocalDate
object annotated with@JsonFormat
withLocalDateSerializer
andLocalDateDeserializer
.
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
LocalDateObj
has no any annotation, theobjectmapper's
registered or default module will be used to serialize and deserialize. - Line 28-31: the
LocalDateJsr310
annotated 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 defaultobjectMapper
is ok to serialize and deserialize when the object has no value for theLocalDate
fields.test_default_throw_exception_with_localDate
– the defaultobjectMapper
throws an exception when serializing or deserializing an object with aLocalDate
field 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
Person
object with aLocalDate
value. - 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
LocalDate
fields, then the defaultobjectMapper
works 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
JavaTimeModule
for theobjectMapper
instance. - Line 27: serialize via
objectMapper.writeValueAsString
. Note, the twoLocalDate
fields have different string formats. - Line 33: deserialize via
objectMapper.readValue
and theLocalDate
s 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
LocalDate
has the default string format pattern. - Line 5: the
LocalDate
has the customized string pattern defined at thePerson
class 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
custLocalDateModule
extends 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
objectMapper
is registed with a newCustLocalDateModule
object.
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
LocalDate
has the customized string format pattern defined at step 5.1. - Line 5: the
LocalDate
has the customized string pattern defined at thePerson
class 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 aSimpleModule
with bothCustLocalDateSerializer
andCustLocalDateDeserializer
and works as expected.test_simpleModule_with_serialize_then_deserialze_got_exception
– register aSimpleModule
and only register withCustLocalDateSerializer
, then it will throw an exception on deserializing aLocalDate
.test_simpleModule_ok_at_null
– register aSimpleModule
with onlyCustLocalDateSerializer
, it’s ok as long as there is no value in theLocalDate
field.
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
addSerializer
andaddDeserializer
to aSimpleModule
and register it to theobjectMapper
. - Line 50, 53: only set up the
addSerializer
to aSimpleModule
and register it to theobjectMapper
. - Line 68, 71: only set up the
addSerializer
to aSimpleModule
and register it to theobjectMapper
. The difference from the second test is that it will throw an exception on the deserialization due to theSimpleModule
not 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
localDateObj
is rendered with format defined by theobjectMapper
‘s module. - Line 5,11,17: the
localDateJsr310
is rendered with format defined at thePerson
class.
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