When working in a java project most often then you would like you will probably find yourself having to deal with dates and times. Before java 8 the standard date library from java had some shortfalls
That forced most projects to rely on external libraries, most often Joda's DateTime library.
The new Java 8 time package introduces new features to the date API making Joda pretty much obsolete.

The new API specification includes the following:

  • java.time - Classes for date, time, date and time combined, time zones, instants, duration, and clocks.
  • java.time.chrono - API for representing calendar systems other than ISO-8601. Several predefined chronologies are provided and you can also define your own chronology.
  • java.time.format - Classes for formatting and parsing dates and time.
  • java.time.temporal - Extended API, primarily for framework and library writers, allowing interoperations between the date and time classes, querying, and adjustment. Fields and units are defined in this package.
  • java.time.zone - Classes that support time zones, offsets from time zones, and time zone rules.
  • JSR 310: Date and Time API

So in our case we ended up with a project that started being developed with java 7 and using Joda's DateTime library to then been updated to the java 8 run time and using the java 8 new time API. Of course, like in every big project updates need to be rolled out in iterative fashion. So first all the new features were being developed using java 8 time's API, then slowly once we got a handle of the new APIs we would start updating the old references to Joda's DateTime and eventually removing its dependency completely from the project.

Application request life-cycle

To give an overview of the request lifecycle of our application.

  • Client makes a HTTP request to our API server
  • API server instantiates a meta object
  • Meta object is persisted on MongoDB or MySQL with OpenJPA
  • Meta object is serialized and sent to another service down the pipeline
  • Response is sent back to client

The old way we were handling serialization and deserialization of DateTime objects would be to annotate the meta objects with the @JsonSerialize and @JsonDeserialize annotations.
We would implement custom serializers and deserializer that would handle the HTTP requests and responses.

@JsonSerialize(using = CustomDateTimeSerializer.class)
@JsonDeserialize(using = CustomDateTimeDeserializer.class)
@Parameter(name = "created", type = ApiParamType.DATETIME, description = "")
protected DateTime created = DateUtil.utcNowDateTime();  

The main issue of annotating the class variables with the @JsonSerialize and @JsonDeserialize is that you can't have different de/seriallization rules for different targets.
In the case where the REST API endpoint returns Dates in one format and MongoDB persists Dates in a different format it is crucial to have the flexibility of configuring different de/serializations strategies for different targets.

Solution

By utilizing Jackson Modules you can pre configure a set of de/serializers for a specific class type thus removing the need of annotating each variable individually.

public class CdosJacksonModule extends SimpleModule {

  public CdosJacksonModule() {
    addSerializer(LocalDateTime.class, new LocalDateTimeSerializer());
    addSerializer(ZonedDateTime.class, new ZonedDateTimeSerializer());

    addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer());
    addDeserializer(ZonedDateTime.class, new ZonedDateTimeDeserializer());
  }

}

By extending the SimpleModule class and adding a custom set of de/serializer(s) by specifing the class type ZoneDateTime.class and the de/serializer implementation ZoneDateTimeSerializer it is possible to register the custom module with a ObjectMapper which will automaticly de/serializer your objects without the need for annotations. The module strategy is the recommended one over the old annotation one.

The implementation for a serializer is the following:

public class ZonedDateTimeSerializer  
        extends StdScalarSerializer<ZonedDateTime> {

  private static Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  public ZonedDateTimeSerializer() {
    super(ZonedDateTime.class);
  }

  @Override
  public void serialize(ZonedDateTime zonedDateTime,
                        JsonGenerator jsonGenerator,
                        SerializerProvider serializerProvider
  ) throws IOException {
jsonGenerator.writeString(zonedDateTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));  
  }
}

For a deserializer

public class ZonedDateTimeDeserializer extends StdScalarDeserializer<ZonedDateTime> {

  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  public ZonedDateTimeDeserializer() {
    super(ZonedDateTime.class);
  }

  @Override
  public ZonedDateTime deserialize(JsonParser jsonParser,
                                   DeserializationContext deserializationContext
  ) throws IOException {
    return ZonedDateTime.parse(jsonParser.getText(), DateTimeFormatter.ISO_OFFSET_DATE_TIME);
  }

}

The most accurate and complete formatter when dealing with dates is the ISOOFFSETDATE_TIME. The formatter parses a date-time with an offset, eg; 2011-12-03T10:15:30+01:00 Since it deal with the numeric offset and not timezone names it can be used as a reliable date type when working in an application that is timezone sensitive.
The DateTimeFormatter also supports a range of different formats.

ISO Dates

ISO stands for International Standard Organization.
When talking about ISO dates what people usually mean is the standard 8601. Mainly it defines a standard way to represent dates, time, timezone, weeks and days in a format that businesses can use around the world.
Some samples:

  • Date: 2015-07-14
  • Combined date and time in UTC:
    • 2015-07-14T10:08:15+00:00
    • 2015-07-14T10:08:15Z
  • Week: 2015-W29
  • Date with week number: 2015-W29-2
  • Ordinal date: 2015-195

MongoDB Jackson Module Serializers/Deserializers

MongoDB only has one type of Date object, that is the ISODate eg; ISODate("2015-07-16T17:43:53.430Z")
The native MongoDB java driver only supports one data type to be persisted as a Date, which is the Date.class. Since our application used ZonedDateTime for dates we had to configure another custom Jackson module to handle persisting and fetching Date objects from MongoDB.

public class MongoJacksonModule extends SimpleModule {

  public MongoJacksonModule() {
    addSerializer(LocalDateTime.class, new MongoLocalDateTimeSerializer());
    addSerializer(ZonedDateTime.class, new MongoZonedDateTimeSerializer());

    addDeserializer(LocalDateTime.class, new MongoLocalDateTimeDeserializer());
    addDeserializer(ZonedDateTime.class, new MongoZonedDateTimeDeserializer());
  }

}

Serializer

public final class MongoZonedDateTimeSerializer extends StdScalarSerializer<ZonedDateTime> {

  public MongoZonedDateTimeSerializer() {
    super(ZonedDateTime.class);
  }

  private static Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  @Override
  public void serialize(ZonedDateTime zonedDateTime,
                        JsonGenerator jsonGenerator,
                        SerializerProvider serializerProvider
  ) throws IOException {
    LocalDateTime localDateTime = zonedDateTime.toLocalDateTime();
    Long epochSecond = localDateTime.toEpochSecond(ZoneOffset.UTC);
    Instant instant = localDateTime.toInstant(ZoneOffset.UTC);
    Date date = Date.from(instant);
    jsonGenerator.writeObject(date);
  }
}

The steps to convert a ZonedDateTime to a Date are the following

  • Grab a LocalDateTime from the ZonedDateTime
  • Convert the LocalDateTime to an Instance which is a wrapper for a unix timestamp
  • Create date from Instance

We always persist the Dates with a UTC timezone on the database and only format to the client's time zone when sending back Dates in the REST API responses

Deserializer

public final class MongoZonedDateTimeDeserializer  
        extends StdScalarDeserializer<ZonedDateTime> {

  private static Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  public MongoZonedDateTimeDeserializer() {
    super(ZonedDateTime.class);
  }

  @Override
  public ZonedDateTime deserialize(JsonParser jsonParser,
                                   DeserializationContext deserializationContext
  ) throws IOException {
    ZonedDateTime zonedDateTime = ZonedDateTime.parse(jsonParser.getText(), DateUtil.dateTimeWithZone);
    // Value returned from MongoDB: Fri Jul 10 16:56:27 UTC 2015
    return zonedDateTime;
  }
}

To Deserialize the Dates stored in Mongo we created a custom DateTimeFormatter:

public static final java.time.format.DateTimeFormatter dateTimeWithZone = java.time.format.DateTimeFormatter.ofPattern("E MMM d H:m:s z uuuu");