There are many pages in Internet that recommend or mention yyyy-MM-dd'T'HH:mm:ss.SSSZ pattern when dealing with ISO-8601 date-times, for instance Spring documentation https://docs.spring.io/autorepo/docs/spring/4.0.2.RELEASE/javadoc-api/org/springframework/format/annotation/DateTimeFormat.ISO.html and Elasticsearch documentation https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html, to name a couple. But this format is actually troublesome when using ISO-8601 format.

Problem 1: timezone

Let’s format a date-time value using this format.

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

    System.out.println(formatter.format(ZonedDateTime.now()));

This produces the following:

2020-08-24T21:49:31.702+0400

Looks good, but it isn’t. The problem is that ISO-8601 dictates that if the date-time part (everything before the timezone designation) uses extended format (that is, contains hyphen and colon as delimiters), then the timezone part must also be in extended format. In our case, the date-time part is in extended format, but the timezone part is in the basic format (as the colon between 04 and 00 is omitted).

If we parse the produced string using ZonedDateTime.parse(), we’ll get an exception:

Exception in thread "main" java.time.format.DateTimeParseException: Text '2020-08-24T21:49:31.702+0400' could not be parsed, unparsed text found at index 26

Another problem is that Z would produce +0000 for UTC, whereas ISO-8601 requires one of the following:

  • either ±HH:mm if any of HH or mm is non-zero
  • or Z to designate UTC

This means that z symbol recommended as an alternative for Z will not work either.

The correct answer is XXX, so the intermediate result will be yyyy-MM-dd'T'HH:mm:ss.SSSXXX.

Why intermediate? Because there is a second problem.

Problem 2: year

Let’s run the following code:

    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
    TemporalAccessor parsed = formatter.parse("2020-08-24T21:49:31.702+04:00");
    System.out.println(ZonedDateTime.from(parsed));

It works fine. But let’s modify it a bit:

    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
            .withResolverStyle(ResolverStyle.STRICT);
    TemporalAccessor parsed = formatter.parse("2020-08-24T21:49:31.702+04:00");
    System.out.println(ZonedDateTime.from(parsed));

We just asked the formatter to be strict when resolving date-time fields. The result follows:

Exception in thread "main" java.time.DateTimeException: Unable to obtain ZonedDateTime from TemporalAccessor: {DayOfMonth=24, OffsetSeconds=14400, YearOfEra=2020, MonthOfYear=8},ISO resolved to 21:49:31.702 of type java.time.format.Parsed

So the formatter has parsed the date-time value, but ZonedDateTime cannot be extracted from this parsed representation!

How year is resolved

To construct a ZonedDateTime, java.time needs to know the exact year. There are two flavors of ‘year’ field:

  • ‘year-of-era’ (this is the familiar yyyy); can only be positive
  • proleptic year (the corresponding pattern is uuuu); can be positive, zero, or negative (so any integer)

Strictly speaking, to resolve a year from year-of-era, java.time needs to know era, naturally. Who knows what was meant by year ‘2000’: is it ‘2000 BC’ or ‘2000 AD’? (By the way, era is denoted by G pattern). But everybody cares about AD years most of the time, so, if we are not so strict, we could still resolve a year from just ‘year-of-era’.

On the other hand, proleptic year is a year how a mathematician would defined it: it may be positive (for years AD) or zero/negative (or years BC). Proleptic year is self-sufficient, it does not need era to be resolved, in any context.

So now it can easily be seen what happened in the example: as soon as we entered strict context, the sole ‘yyyy’ (year-of-era) is not enough anymore, it requires an era which is not supplied (because ISO-8601 format does not use era designation).

Also, it is easy to see how this can be fixed: always use uuuu instead of yyyy, unless you specifically need year-of-era semantics.

Final pattern (so far)

It’s uuuu-MM-dd'T'HH:mm:ss.SSSXXX. This allows to format/parse ISO-8601-compliant date-time values even in strict contexts.