Jackson serializes a ZonedDateTime wrongly in Spring Boot

45,802

Solution 1

There is a library jackson-datatype-jsr310. Try it.

This library covers new datetime API and includes serializers for ZonedDateTime too.

All you need is just to add JavaTimeModule:

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

UPDATE

To convert datetime to ISO-8601 string you should disable WRITE_DATES_AS_TIMESTAMPS feature. You can easily do by either overriding ObjectMapper bean or by using application properties:

spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS = false

Solution 2

If you either don't rely on SpringBoot's auto-configuration feature - you don't provide spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS = false property into your configuration file - or for whatever reason you create ObjectMapper instance manually. You can disable this feature programatically as follows:

ObjectMapper m = new ObjectMapper();
m.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

this is for jackson 2.8.7

Solution 3

The answer was already mentioned above but I think it's missing some info. For those looking to parse Java 8 timestamps in many forms (not just ZonedDateTime). You need a recent version of jackson-datatype-jsr310 in your POM and have the following module registered:

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);

To test this code

@Test
void testSeliarization() throws IOException {
    String expectedJson = "{\"parseDate\":\"2018-12-04T18:47:38.927Z\"}";
    MyPojo pojo = new MyPojo(ZonedDateTime.parse("2018-12-04T18:47:38.927Z"));

    // serialization
    assertThat(objectMapper.writeValueAsString(pojo)).isEqualTo(expectedJson);

    // deserialization
    assertThat(objectMapper.readValue(expectedJson, MyPojo.class)).isEqualTo(pojo);
}

Note that you can configure your object mapper globally in Spring or dropwizard to achieve this. I have not yet found a clean way to do this as an annotation on a field without registering a custom (de)serializer.

Solution 4

For Jackson 2.10 and above,

parent pom.xml

<!-- https://github.com/FasterXML/jackson-bom -->
<dependencyManagement>
  <dependency>
    <groupId>com.fasterxml.jackson</groupId>
    <artifactId>jackson-bom</artifactId>
    <version>2.10.3</version>
    <type>pom</type>
    <scope>import</scope>
  </dependency>
</dependencyManagement>

module pom.xml

<!-- https://github.com/FasterXML/jackson-modules-java8 -->
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

JsonMapper creation, possibly in your @Configuration class

@Bean
public JsonMapper jsonMapper() {
    return JsonMapper.builder()
        .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
        .addModule(new JavaTimeModule())
        .build();
}

Further reading:

Share:
45,802
jbx
Author by

jbx

Updated on July 09, 2022

Comments

  • jbx
    jbx almost 2 years

    I have a simple application with Spring Boot and Jetty. I have a simple controller returning an object which has a Java 8 ZonedDateTime:

    public class Device {
      // ...
      private ZonedDateTime lastUpdated;
    
      public Device(String id, ZonedDateTime lastUpdated, int course, double latitude, double longitude) {
        // ...
        this.lastUpdated = lastUpdated;
        // ...
      }
    
      public ZonedDateTime getLastUpdated() {
        return lastUpdated;
      }
    }
    

    In my RestController I simply have:

    @RequestMapping("/devices/")
    public @ResponseBody List<Device> index() {
      List<Device> devices = new ArrayList<>();
      devices.add(new Device("321421521", ZonedDateTime.now(), 0, 39.89011333, 24.438176666));
    
      return devices;
    }
    

    I was expecting the ZonedDateTime to be formatted according to the ISO format, but instead I am getting a whole JSON dump of the class like this:

    "lastUpdated":{"offset":{"totalSeconds":7200,"id":"+02:00","rules":{"fixedOffset":true,"transitionRules":[],"transitions":[]}},"zone":{"id":"Europe/Berlin","rules":{"fixedOffset":false,"transitionRules":[{"month":"MARCH","timeDefinition":"UTC","standardOffset":{"totalSeconds":3600,"id":"+01:00","rules":{"fixedOffset":true,"transitionRules":[],"transitions":[]}},"offsetBefore":{"totalSeconds":3600,"id":"+01:00","rules":{"fixedOffset":true,"transitionRules":[],"transitions":[]}},"offsetAfter":{"totalSeconds":7200,"id":"+02:00", ...
    

    I just have a spring-boot-starter-web application, using spring-boot-starter-jetty and excluding spring-boot-starter-tomcat.

    Why is Jackson behaving like this in Spring Boot?

    ** UPDATE **

    For those looking for a full step by step guide how to solve this I found this after asking the question: http://lewandowski.io/2016/02/formatting-java-time-with-spring-boot-using-json/

  • jbx
    jbx over 7 years
    Thanks. So I added the jackson-datatype-jsr310 version 2.8.1 dependency, and the effect was that the timestamp changed to a double "lastUpdated":1471893818.177000000. In Spring Boot I don't have direct access to the ObjectMapper it is using, any idea where do I tell it to use the JavaTimeModule? (I am a bit new to Spring Boot)
  • vsminkov
    vsminkov over 7 years
    @jbx check this answer - stackoverflow.com/questions/7854030/… also you need to try turning off WRITE_DATES_AS_TIMESTAMPS feature to convert datetime to ISO-8601
  • jbx
    jbx over 7 years
    Was just going to tell you I found the answer. Yes it works, just by spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS = false, no need to programmatically customise the ObjectMapper. Since my question was specifically about Spring boot, can you edit your answer to add this so that I choose it as the right answer?
  • vsminkov
    vsminkov over 7 years
    @jbx great! glad you solved it completely. Thanks, I'll edit my answer
  • dvelopp
    dvelopp almost 7 years
    Thanks! It helped.
  • UltimaWeapon
    UltimaWeapon over 5 years
    Mine just includes a test that shows how to use the mapper. I know it's not much but I wrote the code myselt to test the solution so I figured I might as well add it in case anyone else was wondering.
  • jbx
    jbx about 4 years
    How is this different from the other answers?
  • Somu
    Somu about 4 years
    From Jackson 2.10 and above ObjectMapper is not recommended any more. This answer is up-to-date with new classes (e.g. JsonMapper) and methods (e.g. addModule).
  • jbx
    jbx about 4 years
    OK thanks for the update. In reality changing the property spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS = false from application.yml is enough.
  • jojo
    jojo about 4 years
    For my solution to my problem, this answer may still be missing the convenience method .findAndRegisterModules(), which is functionally equivalent to mapper.registerModules(findModules()). So when outputting the serialization, it may look like this: new ObjectMapper.findAndRegisterModules().writeValueAsString(req‌​uestObject), where requestObject could be some entity or parameter.
  • MA1
    MA1 over 2 years
    How can I disable it for one field only?
  • Janet
    Janet over 2 years
    this solution worked for my failed mockMVC test. Thanks!