ZonedDateTimeDeserializer is missing in jackson jsr310
Asked Answered
R

2

14

I'm parsing a ZonedDateTime using like this:

 @JsonSerialize(using = ZonedDateTimeSerializer.class)
 private ZonedDateTime expirationDateTime;

I need to be able to properly deserialize this date. However, there is no deserializer for this that is provided by jackson:

com.fasterxml.jackson.datatype.jsr310.deser

Is there a reason why it's missing? What is the most common workaround?

Updated: Here is my scenario:

I create ZonedDateTime like this:

ZonedDateTime.of(2017, 1, 1, 1, 1, 1, 1, ZoneOffset.UTC)

Then I serialize the object that contains the date like this:

public static String toJSON(Object o) {
    ObjectMapper objectMapper = new ObjectMapper();
    StringWriter sWriter = new StringWriter();
    try {
        JsonGenerator jsonGenerator = objectMapper.getJsonFactory().createJsonGenerator(sWriter);
        objectMapper.writeValue(jsonGenerator, o);
        return sWriter.toString();
    } catch (IOException e) {
        throw new IllegalStateException(e);
    }
}

And when I try to send it to Spring MVC Controller:

    mockMvc.perform(post("/endpoint/")
            .content(toJSON(myObject))
            .contentType(APPLICATION_JSON))
            .andExpect(status().isOk());

The date object that goes inside the controller is different.

Before: 2017-01-01T01:01:01.000000001Z

After: 2017-01-01T01:01:01.000000001Z[UTC]

Rodent answered 20/7, 2017 at 11:42 Comment(4)
Jackson by default uses the InstantDeserializer class for ZonedDateTime objects, so it should work. I've made a simple test here and it deserialized the value without any additional configuration. If you have tested and it didn't work, though, you can edit the question and add this info to it.Trudeau
Yes, just a minute.Rodent
How is the deserilzation code in the controller? Do you also use object mapper or it's configured in Spring?Trudeau
Spring boot does it, But, I can see that it even happens when I convert object to/from json via object mapper.Rodent
T
5

The 2 values 2017-01-01T01:01:01.000000001Z and 2017-01-01T01:01:01.000000001Z[UTC] actually represent the same instant, so they are equivalent and can be used without a problem (at least there should be no problems as they represent the same instant).

The only detail is that Jackson, for some reason, sets the ZoneId value to "UTC" when deserializing, which is redundant in this case (the Z already tells that the offset is "UTC"). But it shouldn't affect the date value itself.


A very simple way to get rid of this [UTC] part is to convert this object to OffsetDateTime (so it keeps the Z offset and don't use the [UTC] zone) and then back again to ZonedDateTime:

ZonedDateTime z = // object with 2017-01-01T01:01:01.000000001Z[UTC] value
z = z.toOffsetDateTime().toZonedDateTime();
System.out.println(z); // 2017-01-01T01:01:01.000000001Z

After that, the value of z variable will be 2017-01-01T01:01:01.000000001Z (without the [UTC] part).

But of course this is not ideal as you'd have to do it manually for all dates. A better approach is to write a custom deserializer (by extending com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer) that don't set the timezone when it's UTC:

public class CustomZonedDateTimeDeserializer extends InstantDeserializer<ZonedDateTime> {
    public CustomZonedDateTimeDeserializer() {
        // most parameters are the same used by InstantDeserializer
        super(ZonedDateTime.class,
              DateTimeFormatter.ISO_ZONED_DATE_TIME,
              ZonedDateTime::from,
              // when zone id is "UTC", use the ZoneOffset.UTC constant instead of the zoneId object
              a -> ZonedDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId.getId().equals("UTC") ? ZoneOffset.UTC : a.zoneId),
              // when zone id is "UTC", use the ZoneOffset.UTC constant instead of the zoneId object
              a -> ZonedDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId.getId().equals("UTC") ? ZoneOffset.UTC : a.zoneId),
              // the same is equals to InstantDeserializer
              ZonedDateTime::withZoneSameInstant, false);
    }
}

Then you have to register this deserializer. If you use ObjectMapper, you need to add this to the JavaTimeModule:

ObjectMapper objectMapper = new ObjectMapper();
JavaTimeModule module = new JavaTimeModule();
// add my custom deserializer (this will affect all ZonedDateTime deserialization)
module.addDeserializer(ZonedDateTime.class, new CustomZonedDateTimeDeserializer());
objectMapper.registerModule(module);

If you configure it in Spring, the config will be something like this (not tested):

<bean class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean" id="pnxObjectMapper">
    <property name="deserializersByType">
        <map key-type="java.lang.Class">
            <entry>
                <key>
                    <value>java.time.ZonedDateTime</value>
                </key>
                <bean class="your.app.CustomZonedDateTimeDeserializer">
                </bean>
            </entry>
        </map>
    </property>
</bean>
Trudeau answered 20/7, 2017 at 13:0 Comment(1)
I wish it was built-in feature in the library. Thank you!Rodent
W
2

I use this :

        JavaTimeModule javaTimeModule = new JavaTimeModule();
    javaTimeModule.addSerializer(ZonedDateTime.class, new ZonedDateTimeSerializer(DateTimeFormatter.ISO_DATE_TIME));
    javaTimeModule.addDeserializer(ZonedDateTime.class, InstantDeserializer.ZONED_DATE_TIME);
Wedgwood answered 11/4, 2019 at 13:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.