3

Given the following Unit tests:

@Test
public void zonedDateTimeCorrectlyRestoresItself() {

    // construct a new instance of ZonedDateTime
    ZonedDateTime now = ZonedDateTime.now(ZoneId.of("Z"));
    // offset = {ZoneOffset@3820} "Z"
    // zone   = {ZoneOffset@3820} "Z"

    String converted = now.toString();

    // restore an instance of ZonedDateTime from String
    ZonedDateTime restored = ZonedDateTime.parse(converted);
    // offset = {ZoneOffset@3820} "Z"
    // zone   = {ZoneOffset@3820} "Z"

    assertThat(now).isEqualTo(restored); // ALWAYS succeeds
}

@Test
public void jacksonIncorrectlyRestoresZonedDateTime()  {

    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.findAndRegisterModules();
    objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);

    // construct a new instance of ZonedDateTime
    ZonedDateTime now = ZonedDateTime.now(ZoneId.of("Z"));
    // offset = {ZoneOffset@3820} "Z"
    // zone   = {ZoneOffset@3820} "Z"


    String converted = objectMapper.writeValueAsString(now);

    // restore an instance of ZonedDateTime from String
    ZonedDateTime restored = objectMapper.readValue(converted, ZonedDateTime.class);
    // offset = {ZoneOffset@3820} "Z"
    // zone   = {ZoneOffset@3821} "UTC"

    assertThat(now).isEqualTo(restored); // NEVER succeeds
}

And this workaround:

@Test
public void usingDifferentComparisonStrategySucceeds() throws Exception  {

    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.findAndRegisterModules();
    objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);

    // construct a new instance of ZonedDateTime
    ZonedDateTime now = ZonedDateTime.now(ZoneId.of("Z"));
    // offset = {ZoneOffset@3820} "Z"
    // zone   = {ZoneOffset@3820} "Z"

    String converted = objectMapper.writeValueAsString(now);

    // restore an instance of ZonedDateTime from String
    ZonedDateTime restored = objectMapper.readValue(converted, ZonedDateTime.class);
    // offset = {ZoneOffset@3820} "Z"
    // zone   = {ZoneOffset@3821} "UTC"

    // the comparison succeeds when a different comparison strategy is used
    // checks whether the instants in time are equal, not the java objects
    assertThat(now.isEqual(restored)).isTrue(); 
}

I guess I'm trying to figure out why internally Jackson just doesn't call ZonedDateTime.parse()? Personally I think this is a bug with Jackson but I'm not confident enough to open an issue for it just yet without some feedback.

fIwJlxSzApHEZIl
  • 11,861
  • 6
  • 62
  • 71

1 Answers1

6

Quoting Wikipedia for ISO 8601:

If the time is in UTC, add a Z directly after the time without a space. Z is the zone designator for the zero UTC offset. "09:30 UTC" is therefore represented as "09:30Z" or "0930Z". "14:45:15 UTC" would be "14:45:15Z" or "144515Z".

UTC time is also known as Zulu time, since Zulu is the NATO phonetic alphabet word for Z.

Z is not a Zone. UTC is the Zone, which is then represented using Z in a formatted string.

Don't ever use ZoneId.of("Z"). It's wrong.

Community
  • 1
  • 1
Andreas
  • 154,647
  • 11
  • 152
  • 247
  • 3
    `ZoneId.of("Z")` is not more wrong than according to [the docs](https://docs.oracle.com/javase/8/docs/api/java/time/ZoneId.html#of-java.lang.String-) it should work: “If the zone ID equals 'Z', the result is `ZoneOffset.UTC`. ” – Ole V.V. Jun 05 '17 at 19:37
  • @OleV.V. I was using the docs initially to find the ZoneId of "Z" which is why I was using it in the first place. For now I've just replaced all instances of "Z" with "UTC" but without seeing the Jackson code that instances the ZonedDateTime object it's hard to say if it's a bug or not. – fIwJlxSzApHEZIl Jun 15 '17 at 16:13
  • This problem also occurs if you use the static `ZoneOffset.UTC`, so the solution is not as simple as avoiding `ZoneId.of("Z")`. – MusikPolice Mar 19 '19 at 15:37