5

I am trying to figure out why Jackson (2.9.5) formats dates from Java 8 incorrectly.

data class Test(
        val zonedDateTim: ZonedDateTime = ZonedDateTime.now(),
        val offsetDateTim: OffsetDateTime = OffsetDateTime.now(),
        val date: Date = Date(),
        val localDateTime: LocalDateTime = LocalDateTime.now()
)

val mapper = ObjectMapper().apply {
    registerModule(KotlinModule())
    registerModule(JavaTimeModule())
    dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
    enable(SerializationFeature.INDENT_OUTPUT)
}

println(mapper.writeValueAsString(Test()))

From the date format I provided I would expect to get dates formatted without milliseconds but instead the result looks like this:

{
  "zonedDateTim" : "2018-07-27T13:18:26.452+02:00",
  "offsetDateTim" : "2018-07-27T13:18:26.452+02:00",
  "date" : "2018-07-27T13:18:26",
  "localDateTime" : "2018-07-27T13:18:26.452"
}

Why are milliseconds being included in the formatted dates?

M. Justin
  • 14,487
  • 7
  • 91
  • 130
Daniel
  • 383
  • 1
  • 3
  • 9

2 Answers2

8

dateFormat only applies to Date objects - the 3 other objects are handled by the JavaTimeModule, which uses ISO formatting by default.

If you want a different format, you can use:

val format = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");

val javaTimeModule = JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class, LocalDateTimeSerializer(format));
javaTimeModule.addSerializer(ZonedDateTime.class, ZonedDateTimeSerializer(format));
mapper.registerModule(javaTimeModule);

You may also need to add mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); but I'm not 100% sure that it's necessary.

Also note that with that format, you will lose time zone and offset information. So you may want a different format for Offset/Zoned-DateTimes.

assylias
  • 321,522
  • 82
  • 660
  • 783
  • 1
    Did you test this yourself? Implementing your change has zero impact on the output I get. On top of it find it strange that configuring LocalDateTimeDeserializer should have any impact on ZonedDateTime and OffsetDateTime – Daniel Jul 27 '18 at 11:55
  • @Daniel my example will only change the output for `LocalDateTime`, you need to do the same for the other two. Did it not change the output of `"localDateTime" : "2018-07-27T13:18:26.452"`? – assylias Jul 27 '18 at 12:22
  • No it didn't and as far as I can tell there is not even a default implementation for ZonedDateTime and OffsetDateTime available. – Daniel Jul 27 '18 at 12:32
  • 1
    @Daniel Oh my bad - it should be `javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(format));`. See my edited answer. – assylias Jul 27 '18 at 12:35
  • Thank you for your updated answer. I've accepted it as a correct. For future readers you might want to update your answer one more time because there is no public ctor in OffsetDateTimeSerializer that takes the format as parameter. – Daniel Jul 29 '18 at 08:55
  • @Daniel Indeed - how did you solve the issue? Did you write a custom serializer? – assylias Jul 29 '18 at 10:27
  • 1
    I haven't done it so far but that will be the approach I will take. Since it's more of a side project I didn't have time to work on it yet. – Daniel Jul 30 '18 at 12:00
2

The Answer by assylias is correct. Here are some further thoughts.

Truncate

If you really do not want the fractional second at all, truncate to whole seconds.

Instant instant = Instant.now().truncatedTo( ChronoUnit.SECONDS ) ;
OffsetDateTime odt = OffsetDateTime.now().truncatedTo( ChronoUnit.SECONDS ) ;
ZonedDateTime zdt = ZonedDateTime.now().truncatedTo( ChronoUnit.SECONDS ) ;

The formatters used by the various toString methods by default omit any text representing the fractional second if the value is zero.

So the value of:

2018-07-27T13:18:26.452+02:00

…becomes:

2018-07-27T13:18:26.000+02:00

…and its String representation will be generated as seconds without a fraction:

2018-07-27T13:18:26+02:00

Try it.

OffsetDateTime odt = OffsetDateTime.parse( "2018-07-27T13:18:26.452+02:00" ) ;
OffsetDateTime odtTrunc = odt.truncatedTo( ChronoUnit.SECONDS ) ;

System.out.println( "odt.toString(): " + odt ) ;
System.out.println( "odtTrunc.toString(): " + odtTrunc ) ;

Try that code live at IdeOne.com.

odt.toString(): 2018-07-27T13:18:26.452+02:00

odtTrunc.toString(): 2018-07-27T13:18:26+02:00

Avoid legacy classes

The code in your Question confuses me. Do not mix the troublesome old legacy date-time classes such as Date & SimpleDateFormat with the modern java.time classes. The legacy classes are entirely supplanted by the modern ones.

Timeline

Be clear that LocalDateTime serves a very different purpose than Instant, OffsetDateTime, and ZonedDateTime. A LocalDateTime object purposely lacks any concept of time zone or offset-from-UTC. So it cannot represent a moment, is not a point on the timeline.


About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes.

Where to obtain the java.time classes?

The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154