47

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
  • 21,365
  • 18
  • 90
  • 144
  • If you really want to keep things simple and not trip up the next guy that reads your code, just convert your List to a List> yourself and just put the correctly formatted string in the field you'd like it to be. This way you don't have to familiarize yourself with the ever-changing magical workings of Jackson. I know that might not be an option if Device has many fields, but just wanted to float the option :). – Hans Westerbeek Oct 17 '18 at 16:50
  • Well if you use Spring Boot half the things are magical and can get you tripped over if wrongly configured. The JSR310 configuration has become 'standard' boilerplate you have to do for Java 8 Time support since I posted this question. Maybe in the future the Jackson library will do this by default. – jbx Oct 18 '18 at 09:08

5 Answers5

90

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
vsminkov
  • 10,912
  • 2
  • 38
  • 50
  • 2
    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) – jbx Aug 22 '16 at 19:25
  • @jbx check this answer - http://stackoverflow.com/questions/7854030/configuring-objectmapper-in-spring also you need to try turning off `WRITE_DATES_AS_TIMESTAMPS` feature to convert datetime to ISO-8601 – vsminkov Aug 22 '16 at 19:28
  • 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? – jbx Aug 22 '16 at 19:30
  • @jbx great! glad you solved it completely. Thanks, I'll edit my answer – vsminkov Aug 22 '16 at 19:32
  • Thanks! It helped. – dvelopp Jul 20 '17 at 12:16
  • 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(requestObject)`, where `requestObject` could be some `entity` or parameter. – jojo Apr 12 '20 at 22:16
  • How can I disable it for one field only? – MA1 Aug 23 '21 at 12:53
12

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

bpawlowski
  • 1,025
  • 2
  • 11
  • 22
10

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.

UltimaWeapon
  • 690
  • 9
  • 8
  • 1
    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. – UltimaWeapon Dec 17 '18 at 20:34
  • this solution worked for my failed mockMVC test. Thanks! – Janet Jan 20 '22 at 03:21
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:

Somu
  • 3,593
  • 6
  • 34
  • 44
  • How is this different from the other answers? – jbx Mar 12 '20 at 10:03
  • 2
    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`). – Somu Mar 13 '20 at 05:44
  • OK thanks for the update. In reality changing the property `spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS = false` from `application.yml` is enough. – jbx Mar 13 '20 at 13:29
2

The setting spring.jackson.serialization.write-dates-as-timestamps=false in the application.yml does not help in our project. Most probably since there are additional libraries working with Jackson: Swagger / OpenAPI / OpenAPI Generator.

What has helped is adding this @EventListener for the RequestMappingHandlerAdapter to the @SpringBootApplication class.

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;

@SpringBootApplication
public class SpringBootInitializer {

    @Autowired
    private RequestMappingHandlerAdapter handlerAdapter;

    public static void main(String[] args) {
        SpringApplication.run(SpringBootInitializer.class, args);
    }

    @EventListener
    public void handleContextRefresh(ContextRefreshedEvent event) {
        // see https://github.com/FasterXML/jackson-modules-java8/issues/11#issuecomment-913199874
        // spring.jackson.serialization.write-dates-as-timestamps=false setting does not work in our configuration (probably because of Swagger / OpenAPI / OpenAPI Generator libraries used)

        handlerAdapter
            .getMessageConverters()
            .forEach(c -> {
                if (c instanceof MappingJackson2HttpMessageConverter jsonMessageConverter) {
                    ObjectMapper objectMapper = jsonMessageConverter.getObjectMapper();
                    objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
                }
            });
    }
}

I have found this solution here — https://github.com/FasterXML/jackson-modules-java8/issues/11#issuecomment-913199874.

Dmitriy Popov
  • 2,150
  • 3
  • 25
  • 34