Java

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 – a LocalDate object without any annotation.
  • localDateJsr310 – a LocalDate object annotated with @JsonFormat with LocalDateSerializer and LocalDateDeserializer.

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, the objectmapper's registered or default module will be used to serialize and deserialize.
  • Line 28-31: the LocalDateJsr310 annotated with JSR310 @JsonFormat, LocalDateDeserializer, and LocalDateSerializer. It will be rendered by the JSR310 JavaTimeModule.

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 default objectMapper is ok to serialize and deserialize when the object has no value for the LocalDate fields.
  • test_default_throw_exception_with_localDate – the default objectMapper throws an exception when serializing or deserializing an object with a LocalDate 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 a LocalDate 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 default objectMapper 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 the objectMapper instance.
  • Line 27: serialize via objectMapper.writeValueAsString. Note, the two LocalDate fields have different string formats.
  • Line 33: deserialize via objectMapper.readValue and the LocalDate 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 the Person 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 from SimpleModule.
  • 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 new CustLocalDateModule 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 the Person 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 a SimpleModule with both CustLocalDateSerializer and CustLocalDateDeserializer and works as expected.
  • test_simpleModule_with_serialize_then_deserialze_got_exception – register a SimpleModule and only register with CustLocalDateSerializer, then it will throw an exception on deserializing a LocalDate.
  • test_simpleModule_ok_at_null – register a SimpleModule with only CustLocalDateSerializer, it’s ok as long as there is no value in the LocalDate 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 and addDeserializer to a SimpleModule and register it to the objectMapper.
  • Line 50, 53: only set up the addSerializer to a SimpleModule and register it to the objectMapper.
  • Line 68, 71: only set up the addSerializer to a SimpleModule and register it to the objectMapper. The difference from the second test is that it will throw an exception on the deserialization due to the SimpleModule not adding the deserializer as the first test did.

Ran the Junit tests and captured the results as the following screenshot:

Figure 1. Junit Test Results

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 the objectMapper‘s module.
  • Line 5,11,17: the localDateJsr310 is rendered with format defined at the Person 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.

Download
You can download the full source code of this example here: Jackson Serialize and Deserialize LocalDate Example

Mary Zheng

Mary graduated from the Mechanical Engineering department at ShangHai JiaoTong University. She also holds a Master degree in Computer Science from Webster University. During her studies she has been involved with a large number of projects ranging from programming and software engineering. She worked as a lead Software Engineer where she led and worked with others to design, implement, and monitor the software solution.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button