Normalize zone id during ZonedDateTime deserialization by dscalzi · Pull Request #267 · FasterXML/jackson-modules-java8 (original) (raw)
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.
- 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" (ie2021-02-01T19:49:04.051348600Z[UTC]
instead of2021-02-01T19:49:04.051348600Z
). Note again, that this is default without user intervention. - 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?