Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Normalize zone id during ZonedDateTime deserialization #267

Merged
merged 6 commits into from Feb 21, 2023

Conversation

dscalzi
Copy link
Contributor

@dscalzi dscalzi commented Feb 19, 2023

@dscalzi dscalzi marked this pull request as draft February 19, 2023 22:16
@dscalzi
Copy link
Contributor Author

dscalzi commented Feb 19, 2023

Might have to tweak the solution, thought of another test case and this should probably pass also

    @Test
    public void testDeserializationComparedToStandard2() throws Throwable
    {
        String inputString = "2021-02-01T19:49:04.0513486Z[UTC]";

        assertEquals("The value is not correct.",
                DateTimeFormatter.ISO_ZONED_DATE_TIME.parse(inputString, ZonedDateTime::from),
                READER.readValue(q(inputString)));
    }
   The value is not correct. expected:<2021-02-01T19:49:04.051348600Z[UTC]> but was:<2021-02-01T19:49:04.051348600Z>
   at com.fasterxml.jackson.datatype.jsr310.deser.ZonedDateTimeDeserTest.testDeserializationComparedToStandard2(ZonedDateTimeDeserTest.java:60)

@dscalzi
Copy link
Contributor Author

dscalzi commented Feb 19, 2023

Summary of this as it stands:

ADJUST_DATES_TO_CONTEXT_TIME_ZONE is enabled by default, which is what most users want. The system default (defined in databind) is set to TimeZone.getTimeZone("UTC").

In InstantDeserializer the timezone adjustment happens by default (again, desired behavior).

if (shouldAdjustToContextTimezone(ctxt)) {
    return adjust.apply(value, getZone(ctxt));
}

private ZoneId getZone(DeserializationContext context)
{
    // Instants are always in UTC, so don't waste compute cycles
    return (_valueClass == Instant.class) ? null : context.getTimeZone().toZoneId();
}

From here there are two possible paths.

  1. The consumer does not set a timezone. In this case, the default set by jackson (TimeZone.getTimeZone("UTC")) is used. When going through getZone(), the ZoneId is not normalized, returning the equivalent of ZoneId.of("UTC"). When this is passed to the ZonedDateTime::withZoneSameInstant (value of adjust in the above snippet) we get the issue where the zone is "UTC" instead of "Z" (ie 2021-02-01T19:49:04.051348600Z[UTC] instead of 2021-02-01T19:49:04.051348600Z). Note again, that this is default without user intervention.
  2. The consumer does set a timezone. The question here is should the zone that the user passes be normalized. Arguably, yes. If no normalized zone is available, it will still just use whatever the user set. However if a normalized zone is available, then the resultant ZonedDateTime will match more accurately with a standard timestamp.

Why does this happen? Review the output of the following calls.

toZoneId()

System.out.println(TimeZone.getTimeZone("UTC").toZoneId());  // UTC
System.out.println(TimeZone.getTimeZone(ZoneOffset.UTC).toZoneId()); // UTC
System.out.println(TimeZone.getTimeZone("Z").toZoneId()); // GMT
System.out.println(TimeZone.getTimeZone("GMT").toZoneId()); // GMT

normalized()

System.out.println(TimeZone.getTimeZone("UTC").toZoneId().normalized()); // Z
System.out.println(TimeZone.getTimeZone(ZoneOffset.UTC).toZoneId().normalized()); // Z
System.out.println(TimeZone.getTimeZone("Z").toZoneId().normalized()); // Z
System.out.println(TimeZone.getTimeZone("GMT").toZoneId().normalized()); // Z

ZonedDateTimes with adjustments

String inputString = "2021-02-01T19:49:04.0513486Z";
ZonedDateTime standard = DateTimeFormatter.ISO_ZONED_DATE_TIME.parse(inputString, ZonedDateTime::from);
System.out.println(standard.withZoneSameInstant(ZoneId.of("UTC"))); // 2021-02-01T19:49:04.051348600Z[UTC]
System.out.println(standard.withZoneSameInstant(ZoneId.of("GMT"))); // 2021-02-01T19:49:04.051348600Z[GMT]
System.out.println(standard.withZoneSameInstant(ZoneId.of("Z"))); // 2021-02-01T19:49:04.051348600Z

Fundamentally, there is a disconnect between UTC and GTM here. When the Zone Ids are normalized, all 4 produce the same result, which is consistent with the ISO standard.

The question then boils down to this: If the consumer has set their TimeZone manually to GMT, would they want their ZoneDateTime objects to all say 2021-02-01T19:49:04.051348600Z[GMT]? The current Jackson default would emit 2021-02-01T19:49:04.051348600Z[UTC]. Fundamentally, these are the same times. If the zone were to be normalized, then the result for both of these scenarios would be 2021-02-01T19:49:04.051348600Z. In my opinion, the normalized value is desired.

Reference: FasterXML/jackson-databind#915

I would strongly advocate for changing the default behavior. If needed, I suppose a DeserializationFeature could be implemented to disable normalization of the ZoneId if by the off chance someone is relying on the existing behavior, but it should be opted into during an upgrade.

With normalization on by default, failure of the test case I posted above is expected. With ADJUST_DATES_TO_CONTEXT_TIME_ZONE enabled, the new behavior would be that it is adjusted to a normalized zone.

To read that string in with the zone set to the non-normalized UTC zone, the following test case would work. If a DeserializationFeature to disable normalization were implemented, that could also be used to achieve the desired result.

    @Test
    public void testDeserializationComparedToStandard2() throws Throwable
    {
        String inputString = "2021-02-01T19:49:04.0513486Z[UTC]";

        ZonedDateTime converted = newMapper()
                .configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false)
                .readerFor(ZonedDateTime.class).readValue(q(inputString));

        assertEquals("The value is not correct.",
                DateTimeFormatter.ISO_ZONED_DATE_TIME.parse(inputString, ZonedDateTime::from),
                converted);
    }

Hopefully this now paints a complete picture, since the issue was pretty obscure to start with. What are your thoughts @cowtowncoder?

@dscalzi dscalzi marked this pull request as ready for review February 19, 2023 23:39
@cowtowncoder
Copy link
Member

Hmmh. Ok, this change fails a ton of tests; I can't quite judge if this is expected (some are, probably, but all)?

@dscalzi
Copy link
Contributor Author

dscalzi commented Feb 20, 2023

I'll check, the tests passed locally

@dscalzi
Copy link
Contributor Author

dscalzi commented Feb 20, 2023

I believe the failures are due to the system defaults on the linux box running the test. I'll see if I can mock the environment locally and figure out what it's returning. All the tests pass on my local Windows 11 (EST zone).

@dscalzi
Copy link
Contributor Author

dscalzi commented Feb 20, 2023

That change should fix it. The GitHub runner's system zone is UTC. It worked for me locally because EST normalized is still EST.

@cowtowncoder
Copy link
Member

@dscalzi Ok great.

So, just one more thing wrt merging this: if I haven't yet asked for CLA, from:

https://github.com/FasterXML/jackson/blob/master/contributor-agreement.pdf

I would need that submitted before merging. Only needs to be done once before the first contribution.
The usual way is to print it, fill & sign, scan/photo, email to info at fasterxml dot com.
Once I have it I think I can merge this PR for inclusion 2.15.0 release.

Looking forward to merging!

@dscalzi
Copy link
Contributor Author

dscalzi commented Feb 20, 2023

Just sent, and added a couple notes to the PR

@cowtowncoder cowtowncoder added this to the 2.15.0 milestone Feb 21, 2023
@@ -324,7 +324,9 @@ protected T _fromDecimal(DeserializationContext context, BigDecimal value)
private ZoneId getZone(DeserializationContext context)
{
// Instants are always in UTC, so don't waste compute cycles
return (_valueClass == Instant.class) ? null : context.getTimeZone().toZoneId();
// Normalizing the zone to prevent discrepancies.
// See https://github.com/FasterXML/jackson-modules-java8/pull/267 for details
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good, I like references like this!

@cowtowncoder cowtowncoder changed the title Normalize zone id during ZonedDateTime deserialization. Normalize zone id during ZonedDateTime deserialization Feb 21, 2023
@cowtowncoder cowtowncoder merged commit a9079f3 into FasterXML:2.15 Feb 21, 2023
cowtowncoder added a commit that referenced this pull request Feb 21, 2023
@cowtowncoder
Copy link
Member

Thank you again @dscalzi ! Merged in for inclusion in 2.15.0.

@cowtowncoder
Copy link
Member

Hi @dscalzi !

I just noticed a new failure in

https://github.com/FasterXML/jackson-integration-tests/

(when starting to prepare for upcoming 2.15.0-rc1 release)

where it looks like timezone marker Z becomes ZoneId.of("Z") (instead of formerly ZoneId.of("UTC")).
Not sure if this is intentional; test in question was:

        ZonedDateTime read = MAPPER.readValue(q("2000-01-01T12:00Z"), ZonedDateTime.class);
        assertEquals("The value is not correct.",
                ZonedDateTime.of(2000, 1, 1, 12, 0, 0, 0, ZoneId.of("UTC")),
                read);

but I can "fix" it by changing "UTC" to "Z". But I didn't think "Z" would be a "real" timezone.

@dscalzi
Copy link
Contributor Author

dscalzi commented Mar 14, 2023

hey @cowtowncoder, could you try replacing ZoneId.of("UTC") with ZoneOffset.UTC?

@cowtowncoder
Copy link
Member

Sure, will do (and now I wonder if that wasn't the one already).

@cowtowncoder
Copy link
Member

@dscalzi yes, that fixes it. Thank you!

driessamyn added a commit to corda/corda-runtime-os that referenced this pull request Apr 26, 2023
albrektsson added a commit to navikt/k9-rapid that referenced this pull request Apr 26, 2023
albrektsson added a commit to navikt/k9-rapid that referenced this pull request Apr 26, 2023
* Dep bumps, fjernet jitpack, gradle bump

* Erstattet ZoneId med ZoneOffSet
FasterXML/jackson-modules-java8#267

* Lagt tilbake jitpack
joschi added a commit to joschi/jackson-datatype-threetenbp that referenced this pull request Aug 14, 2023
joschi added a commit to joschi/jackson-datatype-threetenbp that referenced this pull request Aug 14, 2023
joschi added a commit to joschi/jackson-datatype-threetenbp that referenced this pull request Aug 14, 2023
@indyana
Copy link

indyana commented Oct 13, 2023

This change does seem to have broken our application. There are many places where we're comparing ZonedDateTime fields deserialized from stored JSON strings (format written out: "2023-02-02T20:23:11.057Z"), to ZonedDateTime objects created in code with ZoneId.of("UTC") .
Comparing a ZonedDateTime object created by application code to one deserialized from JSON now no longer works because the zones are not equal. (Zone of the deserialized JSON got normalized to "Z").

Fixing seems to mean replacing anywhere in code using ZoneId "UTC" with the offset zone Z, but that's a lot of application code to go through. Would be nice to have a way of disabling normalization!

org.opentest4j.AssertionFailedError:
Expected :2023-02-02T20:23:11.057Z[UTC]
Actual :2023-02-02T20:23:11.057Z

@cowtowncoder
Copy link
Member

cowtowncoder commented Oct 13, 2023

@indyana If you'd like to see configurability, please file a new issue as RFE, asking for configurability, referencing back to this issue. Then if anyone has time and interest they could work on adding this feature -- it does sound useful.

One practical complication here is just that module does not yet have configurability, so would probably need to add a JavaTimeModule.Feature enum and a bit of scaffolding. Aside from that should be quite straight-forward.

@dscalzi
Copy link
Contributor Author

dscalzi commented Oct 15, 2023

@indyana Depending on your usage, it would largely just be a find/replace of ZoneId.of("UTC") with ZoneOffset.UTC as you mentioned.

@indyana
Copy link

indyana commented Oct 15, 2023

Yep, I understand, just a good number of repos to go through to fix the issue and honestly the behavior isn't what I would expect if I specifically configure Jackson to adjust dates to time zone UTC.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Discrepancy in deserialization of ZonedDateTime
3 participants