javaspringspring-bootjackson

Jackson serialization issue with LocalDateTime in Spring Boot Exception Handling


I'm encountering the following error when trying to return a custom ErrorResponse object with a LocalDateTime field using Jackson in my Spring Boot application:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: com.taskflow.taskflow.exceptions.ErrorResponse["timestamp"])

Problem:

Despite adding the necessary Jackson configuration, I still get the error indicating that LocalDateTime is not supported by default.

When I replace ErrorResponse with a Map<String, Object>, it works but that's not how i want to deal with error responses.

What I’ve tried:

Added jackson-datatype-jsr310 dependency.

Registered the JavaTimeModule in the ObjectMapper.

Configured @JsonFormat on the LocalDateTime field in ErrorResponse.

Question:

How can I properly serialize LocalDateTime with Jackson in my custom ErrorResponse class and fix the above error?

AuthenticationEntryPointJwt.java:

@Component
public class AuthenticationEntryPointJwt implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

        ErrorResponse errorResponse = new ErrorResponse(
                HttpServletResponse.SC_FORBIDDEN,
                "Forbidden",
                "You do not have permission to access this resource.",
                request.getRequestURI()
        );

ErrorResponse.java:

@Getter
@Setter
public class ErrorResponse {

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "UTC")
    private LocalDateTime timestamp;
    private int status;
    private String error;
    private String message;
    private String path;

    public ErrorResponse(int status, String error, String message, String path) {
        this.timestamp = LocalDateTime.now();
        this.status = status;
        this.error = error;
        this.message = message;
        this.path = path;
    }
}

I tried adding JacksonConfig but still same issue.

@Configuration
public class JacksonConfig {

    @Bean
    @Primary
    public ObjectMapper objectMapper(){
        return new ObjectMapper()
                .registerModule(new JavaTimeModule()) // Enables LocalDateTime serialization
                .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // Ensures proper formatting
    }
}

pom.xml

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.18.2</version>
</dependency>

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.18.2</version>
</dependency>

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>2.18.2</version>
</dependency>

Solution

  • You're getting the error:

    Java 8 date/time type java.time.LocalDateTime not supported by default

    This happens because Jackson does not support LocalDateTime out of the box. You need to register the JavaTimeModule properly.

    Solution

    1. Ensure You Have the Correct Dependencies Make sure you have the jackson-datatype-jsr310 dependency in your pom.xml:
    
    <dependency>
        <groupId>com.fasterxml.jackson.datatype</groupId>
        <artifactId>jackson-datatype-jsr310</artifactId>
        <version>2.18.2</version>
    </dependency>
    
    1. Register JavaTimeModule in Spring Boot Spring Boot automatically registers JavaTimeModule if the dependency is present. However, in some cases, an explicit configuration is needed.

    Create a Jackson configuration class:

    @Configuration
    public class JacksonConfig {
    
        @Bean
        public ObjectMapper objectMapper() {
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.registerModule(new JavaTimeModule());
            objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
            return objectMapper;
        }
    }
    
    1. Ensure Your ErrorResponse Class Uses @JsonFormat Modify your ErrorResponse class to format LocalDateTime correctly:
    @Getter
    @Setter
    public class ErrorResponse {
    
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "UTC")
        private LocalDateTime timestamp;
        private int status;
        private String error;
        private String message;
        private String path;
    
        public ErrorResponse(int status, String error, String message, String path) {
            this.timestamp = LocalDateTime.now();
            this.status = status;
            this.error = error;
            this.message = message;
            this.path = path;
        }
    }
    
    1. Modify Your Authentication Entry Point Update your AuthenticationEntryPointJwt to use ObjectMapper properly:
    @Component
    public class AuthenticationEntryPointJwt implements AuthenticationEntryPoint {
    
        private final ObjectMapper objectMapper;
    
        @Autowired
        public AuthenticationEntryPointJwt(ObjectMapper objectMapper) {
            this.objectMapper = objectMapper;
        }
    
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    
            ErrorResponse errorResponse = new ErrorResponse(
                    HttpServletResponse.SC_FORBIDDEN,
                    "Forbidden",
                    "You do not have permission to access this resource.",
                    request.getRequestURI()
            );
    
            // Convert ErrorResponse to JSON
            response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
        }
    }