1

I've been dealing with a nasty heisenbug lately. It reminded me of the famous 500-mile email story. I parse a date received in what is pretty much ISO 8601, but the parsed Date object is sometimes many minutes in the future. However, when I try to parse the same data in a unit test, the resulting date is always correct.

Here is the format I receive:

2018-06-25T13:50:53.984771

I parse it from JSON using Jackson:

new StdDateFormat().parse(timestamp);

Here are some examples of parsed dates:

2018-06-25T13:50:59.337828
Mon Jun 25 15:56:36 CEST 2018

2018-06-25T13:50:53.984771
Mon Jun 25 16:07:17 CEST 2018

The two-hour difference is fine, that's just timezone, but what about the minutes? They appear to be randomly offset!

JohnEye
  • 6,436
  • 4
  • 41
  • 67
  • Kind of duplicate of [Changing Java Timestamp format causes a change of the timestamp](https://stackoverflow.com/questions/50908447/changing-java-timestamp-format-causes-a-change-of-the-timestamp) (and more). – Ole V.V. Jun 26 '18 at 07:03

3 Answers3

3

I am answering my own question hoping it will save somebody's skin one day.

The problem did not manifest in unit tests, so I thought that perhaps there was something wrong with the server. I tried everything I thought could be wrong with it, including research of weird timezones which are offset by 15 minutes, investigating where Java actually takes time from and so on. In the end, I had no other choice but to deploy several modifications to the code to bisect where the problem could be. It turned out to be the parser.

Here's the bug which was causing the issue: https://github.com/FasterXML/jackson-databind/issues/1668

I was only able to find it when a friend noticed that the number of milliseconds at the end of the timestamp corresponds to the offset in minutes:

Original: 2018-06-25T13:50:53.984771
Milliseconds: 984771 ms = 16 minutes 24.77 seconds
Result: Mon Jun 25 16:07:17 CEST 2018

The problem was that the parser counted the last part of the String as milliseconds and silently added them to an internal Calendar object, which happily accepts any number of milliseconds and adds them to itself as designed.

The reason why the problem did not manifest in unit tests is that the library I created uses a newer version of Jackson which no longer contains the bug. However, the application using the library already contained Jackson, and since Maven picks the definition of the library which is closest to the project being built when resolving dependencies, it overrode my newer version of Jackson in the library with an older one declared in its own pom.xml. So in the end it was a case of self-inflicted dependency hell.

JohnEye
  • 6,436
  • 4
  • 41
  • 67
  • I don't understand your comment: `.. and since Maven prefers the topologically closest declaration of libraries, it overrode my newer version of Jackson with an older one declared in its pom.xml...`... – khmarbaise Jun 25 '18 at 18:18
  • @khmarbaise I tried to reword the answer to make it clearer, but since I'm not sure whether it's any better, I also added a link to a manual page which explains what happened. – JohnEye Jun 25 '18 at 18:39
2

java.time

You are using the terribly troublesome old date-time classes (Date, Calendar) from the earliest versions of Java. These classes are now legacy, supplanted years ago by the java.time classes.

I'm not a Jackson user, so I do not know if Jackson has added built-in support for java.time yet. But I imagine it is likely.

If not, it looks like you can add a support module to Jackson for this purpose. See this article, this Question, and this GitHub site.

The java.time classes use nanosecond resolution. That means up to nine (9) decimal digits of fractional second. More than enough for the 6 digits in your inputs.

Zoned vs unzoned

You have another problem, of a different nature. You are taking input for a date-time lacking any concept of time zone or offset-from-UTC. Then you are saving it into a different type, a type with a time zone. Not good. You have injected value where none was deserved. This is like taking a generic number assumed to be a price, and then applying arbitrarily a particular currency not indicated in the input.

You should be parsing an input such as 2018-06-25T13:50:53.984771 as a LocalDateTime. This type purposely lacks any time zone or offset-from-UTC, like your input. This type does not represent a specific moment, is not a point on the timeline, as it has no real meaning until you apply the context of a zone/offset.

For more discussion, search Stack Overflow for LocalDateTime, ZonedDateTime, OffsetDateTime, and Instant. In particular for some Answers of mine, search for "Santa".


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
  • Heh, I saw several of your answers and I was wondering whether you would show up ;-) I only realized I was using the outdated API when I read one of your answers. You repeat a lot of content in those, maybe it would be better if you created some kind of a summary blog post and link to that instead, the SO answers are quite terse and I found that it's not easy to find a quick reference which would cover the most common use cases, such as "I have just this UTC timestamp and want to parse it". – JohnEye Jun 26 '18 at 11:47
1

Use jackson-modules-java8 and the java.time types recommended by Basil Bourque in his answer. I haven’t got experience with jackson-modules-java8, but it should definitely parse fractions of seconds with up to at least 9 decimals correctly, and it does support the modern date-time types.

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161