Overview
This article was originally written to understand how Spring Boot2 specifically serializes and deserializes the JSR 310 datetime system, Spring MVC application scenarios are as follows.
- using
@RequestBody
to read the JSON request body from the client and encapsulate it into a Java object.
- use
@ResponseBody
to serialize the object into JSON data and respond to the client.
For some basic types of data like Integer, String, etc., Spring MVC can solve it with some built-in converters without user concern, but for datetime types (e.g. LocalDateTime
), due to the variable format, there are no built-in converters available, so you need to configure and handle it yourself.
Reading this article assumes that the reader has an initial understanding of how to use Jackson.
Test environment
This article uses Spring Boot version 2.6.6 and the Jackson version used is as follows.
1
|
<jackson-bom.version>2.13.2.20220328</jackson-bom.version>
|
Jackson needs to import the following dependencies to process JSR 310 datetime.
1
2
3
4
5
|
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.13.2</version>
</dependency>
|
Spring Boot autoconfiguration
Jackson is automatically configured in the spring-boot-autoconfigure package.
1
2
3
4
5
6
7
|
package org.springframework.boot.autoconfigure.jackson;
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ObjectMapper.class)
public class JacksonAutoConfiguration {
// Detailed code omitted
}
|
One piece of code configures the ObjectMapper
.
1
2
3
4
5
6
|
@Bean
@Primary
@ConditionalOnMissingBean
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.createXmlMapper(false).build();
}
|
You can see that ObjectMapper
is built by Jackson2ObjectMapperBuilder
.
Further down you will see the following code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
static class JacksonObjectMapperBuilderConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder(ApplicationContext applicationContext,
List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.applicationContext(applicationContext);
customize(builder, customizers);
return builder;
}
private void customize(Jackson2ObjectMapperBuilder builder,
List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
for (Jackson2ObjectMapperBuilderCustomizer customizer : customizers) {
customizer.customize(builder);
}
}
}
|
It is found that the Jackson2ObjectMapperBuilder
is created here and the customize(builder, customizers)
method is called, passing in List<Jackson2ObjectMapperBuilderCustomizer>
to customize the ObjectMapper.
Jackson2ObjectMapperBuilderCustomizer
is an interface with only one method and the source code is as follows.
1
2
3
4
5
6
7
8
9
10
|
@FunctionalInterface
public interface Jackson2ObjectMapperBuilderCustomizer {
/**
* Customize the JacksonObjectMapperBuilder.
* @param jacksonObjectMapperBuilder the JacksonObjectMapperBuilder to customize
*/
void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder);
}
|
To put it simply, Spring Boot collects all the Jackson2ObjectMapperBuilderCustomizer
implementation classes inside the container and unifies the Jackson2ObjectMapperBuilder
settings to customize the ObjectMapper. So if we want to customize ObjectMapper
, we just need to implement the Jackson2ObjectMapperBuilderCustomizer
interface and register it to the container.
Customizing the Jackson configuration class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
@Component
public class JacksonConfig implements Jackson2ObjectMapperBuilderCustomizer, Ordered {
/** Default Date Time Format */
private final String dateTimeFormat = "yyyy-MM-dd HH:mm:ss";
/** Default date format */
private final String dateFormat = "yyyy-MM-dd";
/** Default time format */
private final String timeFormat = "HH:mm:ss";
@Override
public void customize(Jackson2ObjectMapperBuilder builder) {
// Set the format of serialization and deserialization of the java.util.Date.
builder.simpleDateFormat(dateTimeFormat);
// JSR 310 Date Time Processing
JavaTimeModule javaTimeModule = new JavaTimeModule();
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormat);
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(dateTimeFormatter));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(dateTimeFormatter));
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(dateFormat);
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(dateFormatter));
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(dateFormatter));
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern(timeFormat);
javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(timeFormatter));
javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(timeFormatter));
builder.modules(javaTimeModule);
// global configuration for serializing Long types to String, which solves the problem of lost precision of JSs numeric types in the browser client.
builder.serializerByType(BigInteger.class, ToStringSerializer.instance);
builder.serializerByType(Long.class,ToStringSerializer.instance);
}
@Override
public int getOrder() {
return 1;
}
}
|
This configuration class implements three types of personalized configurations.
- setting the format of serialization and deserialization of the
java.util.Date
class.
- JSR 310 datetime processing.
- global configuration for serializing
Long
types to String
, which solves the problem of lost precision of JSs numeric types in the browser client.
Of course, the reader can continue to customize other configurations according to their own needs.
Testing
Here the test is done with JSR 310 date and time.
Create a User class
1
2
3
4
5
6
7
8
9
10
|
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
private String name;
private LocalDate localDate;
private LocalTime localTime;
private LocalDateTime localDateTime;
}
|
Create a UserController
1
2
3
4
5
6
7
8
9
10
11
|
@RestController
@RequestMapping("user")
public class UserController {
@PostMapping("test")
public User test(@RequestBody User user){
System.out.println(user.toString());
return user;
}
}
|
Request body
1
2
3
4
5
6
7
|
{
"id": 184309536616640512,
"name": "spring boot",
"localDate": "2023-03-01",
"localTime": "09:35:50",
"localDateTime": "2023-03-01 09:35:50"
}
|
Response Body
1
2
3
4
5
6
7
|
{
"id": "184309536616640512",
"name": "spring boot",
"localDate": "2023-03-01",
"localTime": "09:35:50",
"localDateTime": "2023-03-01 09:35:50"
}
|
As you can see, the client post what data, the back-end response to what data. The only difference is that the id in the response json is now a string, which prevents JavaScript from losing precision.
We also see that the date and time types such as LocalDateTime
are serialized and deserialized according to the format we specify.
Cannot deserialize value of type java.time.LocalDateTime
If JacksonConfig is not configured, Spring MVC will throw the following exception after trying the built-in converter without success.
1
|
JSON parse error: Cannot deserialize value of type java.time.LocalDateTime
|
At this point, the response to the client is as follows.
1
2
3
4
5
6
|
{
"timestamp": "2023-03-01T09:53:02.158+00:00",
"status": 400,
"error": "Bad Request",
"path": "/user/test"
}
|
Summary
The core ObjectMapper class
ObjectMapper
is one of the most important classes of the jackson-databind module, which performs almost all the functions of data processing.
Spring MVC relies on ObjectMapper
to handle client-side JSON request bodies, complete with serialization and deserialization. Therefore, it is only necessary to customize the ObjectMapper
provided by Spring Boot by default.
Don’t override the default configuration
We customize by implementing the Jackson2ObjectMapperBuilderCustomizer
interface and registering it to the container, Spring Boot does not override the default ObjectMapper
configuration, but rather merges and enhances it, which will also be sorted according to the Jackson2ObjectMapperBuilderCustomizer
implementation class Order priority, so the above JacksonConfig configuration class also implements the Ordered
interface.
The default Jackson2ObjectMapperBuilderCustomizerConfiguration
priority is 0, so if we want to override the configuration, just set the priority greater than 0.
Note: In SpringBoot2 environment, do not register custom ObjectMapper
objects to the container, this will overwrite the original ObjectMapper
configuration!
Reference: https://segmentfault.com/a/1190000043498796