Resolve LocalDateTime exception when json string is converted to object

Keywords: Java JSON SpringBoot

1 An exception occurred

This exception occurs in the front-end sending request body with two dates in the back-end entity class, which are formatted as LocalDateTime in the JDK8 time class.By default, LocalDateTime can only parse strings in standard format such as 2020-01-01T10:00:00, where there is a T between the date and time.Without any modifications, the LocalDateTime directly resolves 2020-05-01 08:00:00, a date format we are accustomed to accepting, throwing an exception.

Exception information:

org.springframework.http.converter.HttpMessageNotReadableException: Invalid JSON input: Cannot deserialize value of type `java.time.LocalDateTime` from String "2020-05-04 00:00": Failed to deserialize java.time.LocalDateTime: (java.time.format.DateTimeParseException) Text '2020-05-04 00:00' could not be parsed at index 10; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.time.LocalDateTime` from String "2020-05-04 00:00": Failed to deserialize java.time.LocalDateTime: (java.time.format.DateTimeParseException) Text '2020-05-04 00:00' could not be parsed at index 10

// Omit some exception information

Caused by: java.time.format.DateTimeParseException: Text '2020-05-04 00:00' could not be parsed at index 10

// Omit some exception information

From the exception information, we can see that there is a problem parsing the position of index 10 from 2020-05-04 00:00, because the 10th place is a space here, and the 10th place is a T in the standard LocalDateTime format.

2 Problem Description

The question now is:

  • The back end uses the LocalDateTime class.What are the advantages of the LocalDateTime class over the previous Cate class? The online data is already very detailed.
  • The data returned from the front end may be yyyy-MM-dd HH:mm:ss or yyyy-MM-dd HH:mm, but it certainly will not be yyyy-MM-ddTHH:mm:ss.That is, the format of the date returned by the front end is indeterminate. It may be the time of the year, month, day, second, month and day, or any other date format that is commonly used.Obviously, it won't be the T-minute, T-minute, T-minute, T-second of the year, because this front end requires additional conversion and is completely out of line with human usage.

3 tried methods

My SpringBoot version is 2.2.5.

3.1 @JsonFormat

Add @JsonFormat (pattern = yyyy-MM-dd HH:mm:ss) and timezone = GMT+8) to the field of the entity class.

This method solves the problem, but the disadvantage is that it annotates every occurrence, does not configure globally, and can only set a format that does not meet my needs.

3.2 Register Converter<String, LocalDateTime>implementation class as bean

Result: Not effective.This method solves the inversion of the @RequestParam parameter of the controller layer's method.

It was later discovered that this scheme was used for the parameters of the control layer method.This is the following scenario:

@GetMapping("/test")
public void test(@RequestParam("time") LocalDateTime time){
    // Omit Code
}

3.3 Register the implementation class of Formatter<LocalDateTime>as a bean

Result: Not effective.

It was later discovered that this scheme was also used for control layer method parameters.

4 Solve problems

Reference material: bug resolution for json conversion to LocalDateTime failed in springboot

First, we need to know that SpringBoot uses Jackson for serialization by default.From the blog, we can see that converting dates in JSON strings from string format to the LocalDateTime class is done by the deserialize() method of the com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer class.This can be confirmed by breakpoint debugging

The solution is to replace the deserializer inside jackson with a custom deserializer and use your own defined parsing logic when parsing.

Here, serialize refers to the operation of converting a Java object into a json string, while deserialize refers to the operation of parsing a json string into a Java object.The problem to solve now is deserialization.

4.1 Entity Class

public class LeaveApplication {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private Long proposerUsername;
    // LocalDateTime class
    private LocalDateTime startTime;
    // LocalDateTime class
    private LocalDateTime endTime;
    private String reason;
    private String state;
    private String disapprovedReason;
    private Long checkerUsername;
    private LocalDateTime checkTime;

    // Omit getter, setter
}

4.2 controller Layer Method

@RestController
public class LeaveApplicationController {
    private LeaveApplicationService leaveApplicationService;

    @Autowired
    public LeaveApplicationController(LeaveApplicationService leaveApplicationService) {
        this.leaveApplicationService = leaveApplicationService;
    }

    /**
     * Students initiate leave applications
     * job and trigger are only formed when you agree to apply for leave by inserting a piece of data into the application form
     */
    @PostMapping("/leave_application")
    public void addLeaveApplication(@RequestBody LeaveApplication leaveApplication) {
        leaveApplicationService.addLeaveApplication(leaveApplication);
    }

}

4.3 Customize LocalDateTimeDeserializer

Copy the com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer class as a whole.Note here that I used the original class name, so if I copy the code directly, there will be class name conflicts and IDEA automatically imports ```com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer`, just remove the prefix of the class.

public class LocalDateTimeDeserializer extends JSR310DateTimeDeserializerBase<LocalDateTime> {
    
    // Omit code that does not need to be modified

    /**
     * Key Methods
     */
    @Override
    public LocalDateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException {
        if (parser.hasTokenId(6)) {
            // Modify the code inside this branch
            String string = parser.getText().trim();
            if (string.length() == 0) {
                return !this.isLenient() ? (LocalDateTime) this._failForNotLenient(parser, context, JsonToken.VALUE_STRING) : null;
            } else {
                return convert(string);
            }
        } else {
            // Code that was not modified was omitted
        }
    }

    public LocalDateTime convert(String source) {
        source = source.trim();
        if ("".equals(source)) {
            return null;
        }
        if (source.matches("^\\d{4}-\\d{1,2}$")) {
            // yyyy-MM
            return LocalDateTime.parse(source + "-01 00:00:00", dateTimeFormatter);
        } else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2}$")) {
            // yyyy-MM-dd
            return LocalDateTime.parse(source + " 00:00:00", dateTimeFormatter);
        } else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}$")) {
            // yyyy-MM-dd HH:mm
            return LocalDateTime.parse(source + ":00", dateTimeFormatter);
        } else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}:\\d{1,2}$")) {
            // yyyy-MM-dd HH:mm:ss
            return LocalDateTime.parse(source, dateTimeFormatter);
        } else {
            throw new IllegalArgumentException("Invalid datetime value '" + source + "'");
        }
    }
}

During this process, I improved the method in my blog by parsing the use of strings, using regular expressions to determine the actual format of the date, and then parsing the string into LocalDateTime.This method makes the conversion process compatible with multiple date types and achieves the desired results.

4.4 Replace Deserializer

But the way I replaced it in my blog did not work.When deserializing,

@Configuration
public class LocalDateTimeSerializerConfig {
   @Bean
   public ObjectMapper serializingObjectMapper() {
    JavaTimeModule module = new JavaTimeModule();
       // Select your own LocalDateTimeDeserializer when importing packages here
    LocalDateTimeDeserializer dateTimeDeserializer = new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    module.addDeserializer(LocalDateTime.class, dateTimeDeserializer);
    return Jackson2ObjectMapperBuilder.json().modules(module)
        .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).build();
  }
}

4.5 Replace Deserializer Again

I set out to look up the data again and found a question and answer on the powerful stack overflow: How to custom a global jackson deserializer for java.time.LocalDateTime.

// This is a configuration class for webmvc
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    // Rewrite configureMessageConverters
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        JavaTimeModule module = new JavaTimeModule();
        // Serializer
        module.addSerializer(LocalDateTime.class,
                new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        // deserializer
        // Custom deserializer added here
        module.addDeserializer(LocalDateTime.class,
                new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(module);

        // add converter at the very front
        // if there are same type mappers in converters, setting in first mapper is used.
        converters.add(0, new MappingJackson2HttpMessageConverter(mapper));
    }
}

Run the program at this time, find or not, do not follow the custom deserializer.But at this point, I see this sentence in the original Q&A if there is a same type mappers in converters, setting in first mapper is used. It means that if there is a mapper of the same type in converter, then the one set first will take effect.

Then I recall that when the return value format was uniform, an exception would have been thrown if the return value was of type String.To solve this problem, I override extendMessageConverters() in the webmvc configuration.

@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
	converters.add(0, new MappingJackson2HttpMessageConverter());
}

Probably something went wrong here, so I'll comment this out first.If so, run the program again, and the date resolution goes to the custom deserializer.In the meantime, you can see that converters.add() is called in both methods, so the problem of returning a String before that will not happen again.

At this point, the problem of parsing exceptions when the date in the json string is resolved to LocalDateTime is completely resolved.

This article is distributed by blogs and other operating tool platforms OpenWrite Release

Posted by hawk72500 on Thu, 07 May 2020 18:53:22 -0700