Sydney
Sydney

Reputation: 12212

Java 8 date time types serialized as object with Spring Boot

I have an entity with fields of Java 8 date time types. The issue is that these fields are serialized as object. I added the jackson-datatype-jsr310 dependency, so Spring Boot 1.5.7 would auto configure the JavaTimeModule that handles Java 8 date time types. It seems that the module is not registered (I put a breakpoint in JavaTimeModule constructor). I know I don't need a custom ObjectMapper. I spent hours reading about that issue and the solution is always to add the jackson-datatype-jsr310 dependency but it does not work in my case.

The entity:

@Entity
public class DateTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private LocalDate localDate;

    private LocalDateTime localDateTime;

    private Instant instant;

    private OffsetDateTime offsetDateTime;

    private ZonedDateTime zonedDateTime;

}

The RestController method:

@GetMapping("/datetimes/{id}")
public ResponseEntity<DateTimeEntity> getById(@PathVariable Long id) {
    DateTimeEntity dateTimeEntity = dateTimeRepository.findOne(id);
    return new ResponseEntity<DateTimeEntity>(dateTimeEntity, HttpStatus.OK);

}

The JSON object returned:

    {
    "id": 1,
    "localDate": null,
    "localDateTime": null,
    "instant": {
        "epochSecond": 1508772600,
        "nano": 0
    },
    "offsetDateTime": {
        "offset": {
            "totalSeconds": 0,
            "id": "Z",
            "rules": {
                "fixedOffset": true,
                "transitionRules": [],
                "transitions": []
            }
        },
        "dayOfMonth": 23,
        "dayOfWeek": "MONDAY",
        "dayOfYear": 296,
        "month": "OCTOBER",
        "monthValue": 10,
        "year": 2017,
        "hour": 15,
        "minute": 30,
        "nano": 0,
        "second": 0
    },
    "zonedDateTime": {
        "offset": {
            "totalSeconds": 0,
            "id": "Z",
            "rules": {
                "fixedOffset": true,
                "transitionRules": [],
                "transitions": []
            }
        },
        "zone": {
            "totalSeconds": 0,
            "id": "Z",
            "rules": {
                "fixedOffset": true,
                "transitionRules": [],
                "transitions": []
            }
        },
        "dayOfMonth": 23,
        "dayOfWeek": "MONDAY",
        "dayOfYear": 296,
        "month": "OCTOBER",
        "monthValue": 10,
        "year": 2017,
        "hour": 15,
        "minute": 30,
        "nano": 0,
        "second": 0,
        "chronology": {
            "id": "ISO",
            "calendarType": "iso8601"
        }
    }
}

The POM file:

<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>framework-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
        <mockito.version>2.11.0</mockito.version>
        <org.mapstruct.version>1.2.0.Final</org.mapstruct.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-jdk8</artifactId>
            <version>${org.mapstruct.version}</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>${jackson.version}</version>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${org.mapstruct.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.asciidoctor</groupId>
                <artifactId>asciidoctor-maven-plugin</artifactId>
                <version>1.5.5</version>
                <executions>
                    <execution>
                        <id>output-html</id>
                        <phase>generate-resources</phase>
                        <goals>
                            <goal>process-asciidoc</goal>
                        </goals>
                        <configuration>
                            <backend>html</backend>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

Upvotes: 23

Views: 38006

Answers (7)

LovaBill
LovaBill

Reputation: 5139

If you have issue with Spring Batch Jackson2ExecutionContextStringSerializer, the solution is:

@Configuration
public class JacksonConfig {

@Bean
public BatchConfigurer batchConfigurer(DataSource dataSource, PlatformTransactionManager transactionManager) {
    return new DefaultBatchConfigurer(dataSource) {
        @Override
        protected JobRepository createJobRepository() throws Exception {
            Jackson2ExecutionContextStringSerializer serializer = new Jackson2ExecutionContextStringSerializer();
            ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()).findAndRegisterModules();
            objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
            serializer.setObjectMapper(objectMapper);

            JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
            factory.setDataSource(dataSource);
            factory.setTransactionManager(transactionManager);
            factory.setSerializer(serializer);
            return factory.getObject();
        }
    };
}

}

Source: https://docs.spring.io/spring-batch/docs/current/reference/html/job.html

Upvotes: 1

Piotr Żak
Piotr Żak

Reputation: 2413

You can also try to use

ObjectMapper objectMapper = JsonMapper.builder()
        .addModule(new JavaTimeModule())
        .build();

JavaTimeModule is from package com.fasterxml.jackson.datatype.jsr310 I have used library jackson-datatype-jsr310-2.13.3.jar

Upvotes: 9

Thomas Auinger
Thomas Auinger

Reputation: 2095

Spring Data Couchbase introduced a @Bean factory method in class AbstractCouchbaseConfiguration, which produces an ObjectMapper bean so that Spring does not create its own and correct one (automatically including the java-time module stuff).

Here is the offending source file: https://github.com/spring-projects/spring-data-couchbase/blame/4.2.x/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java#L309

The bug report is here: https://github.com/spring-projects/spring-data-couchbase/issues/1209

This has been fixed in Spring-Data-Couchbase 4.3

Upvotes: 0

valc
valc

Reputation: 111

In case you are currently using String Boot 2.5.4 + Apache CXF (Java config) for JAX-RS, the below config solved problem for me

dependencies{
    constraints{
            implementation("org.apache.cxf:cxf-spring-boot-starter-jaxrs:3.4.4")
            implementation("com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.12.5")
            implementation("com.fasterxml.jackson.module:jackson-modules-java8:2.12.5")
    }
}

dependencies{
    implementation("org.apache.cxf:cxf-spring-boot-starter-jaxrs")
    implementation("com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider")
    implementation("com.fasterxml.jackson.module:jackson-modules-java8")
}

Spring Boot (org.springframework.http.converter.json.Jackson2ObjectMapperBuilder), automatically registers the following well-known modules if they are detected on the classpath:

  • jackson-datatype-jdk8 : support for other Java 8 types like java.util.Optional
  • jackson-datatype-jsr310 : support for Java 8 Date & Time API types
  • jackson-datatype-joda : support for Joda-Time types
  • jackson-module-kotlin : support for Kotlin classes and data classes are auto-configured in case the needed class are on classpath

But to use CXF it seems we need to initialize it with correct serialization provider. And in case we not pass Spring Boot configured ObjectMapper to the JacksonJsonProvider we will catch the error like this:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type 'java.time.Instant' not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling

to fix it we have to init JacksonJsonProvider with Spring configured ObjectMapper, like the code below

    @Autowired
    DataService dataService;
    @Autowired
    EventService eventService;
    @Autowired
    SessionService sessionService;
    @Autowired
    StatusService statusService;

    @Autowired
    private Bus bus;
    @Autowired
    private ObjectMapper objectMapper;

    @Bean
    public JacksonJsonProvider jsonProvider() {
        JacksonJsonProvider provider = new JacksonJsonProvider();
        provider.setMapper(objectMapper);
        return provider;
    }


    @Bean
    public Server rsServer() {
        JAXRSServerFactoryBean server = new JAXRSServerFactoryBean();
        server.setProviders(Stream.of(jsonProvider()).collect(Collectors.toList()));
        server.setBus(bus);
        server.setServiceBeans(Stream
                .of(dataService, eventService, sessionService, statusService)
                .collect(Collectors.toList()));
        return server.create();
    }

Upvotes: 1

TheJeff
TheJeff

Reputation: 4101

I started having this issue in the upgrade of spring boot from 2.3.7 to 2.5.1.

If you have an ObjectMapper @Bean defined, then you'll want to register the time module with it.

@Bean
public ObjectMapper defaultMapper() {
    ObjectMapper objectMapper = new ObjectMapper(); 
    objectMapper.registerModule(new JavaTimeModule()); 
    return objectMapper;
}

A lot of the time, coders will just create a "new ObjectMapper()" when using jackson serialization so keep an eye out for using the vanilla mapper, rather than autowiring a pre-configured default that has the time module registered.

As mentioned, you'll need the jackson-datatype-jsr310, but this is included in spring boot as a managed version.

If you don't define an object mapper bean manually, then spring boot should automatically provide one with the time module registered.

Upvotes: 42

Sydney
Sydney

Reputation: 12212

The solution was to add the dependency to the classpath. For some reason, it was not in the IDE.

Even though the dependency is deprecated, it's still used by spring-boot-autoconfigure module. See Spring Boot Code

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

when it's in the classpath, Java 8 date and time objects are serialized as a timestamp.

Upvotes: 4

Baptiste Beauvais
Baptiste Beauvais

Reputation: 2086

According to How to customize ObjectMapper :

Any beans of type com.fasterxml.jackson.databind.Module will be automatically registered with the auto-configured Jackson2ObjectMapperBuilder and applied to any ObjectMapper instances that it creates. This provides a global mechanism for contributing custom modules when you add new features to your application.

Just adding the dependancy is not enough, you have to declare a @Bean of you module like follow:

@Bean
public Module dateTimeModule(){
    return new JavaTimeModule();
}

Plus jackson-datatype-jsr310 module is deprecated you should use JavaTimeModule instead.

Upvotes: 7

Related Questions