FasterXML / jackson-databind

General data-binding package for Jackson (2.x): works on streaming API (core) implementation(s)
Apache License 2.0
3.53k stars 1.39k forks source link

XMLGregorianCalendar cannot serialize/deserialize timezone #1791

Open dukeyin opened 7 years ago

dukeyin commented 7 years ago

I'm working with an XML object and can't serialize or deserialize date/times with defined timezones. This was tested with Jackson versions:

Unexpected serialization:

XMLGregorianCalendar xmlDateTime = new XMLGregorianCalendarImpl();
xmlDateTime.setYear(2017);
xmlDateTime.setMonth(10);
xmlDateTime.setDay(31);
xmlDateTime.setHour(13);
xmlDateTime.setMinute(45);
xmlDateTime.setSecond(59);
xmlDateTime.setTimezone(-4 * 60);

System.out.println(xmlDateTime.toString());
// Original value: 2017-10-31T13:45:59-04:00

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);

try
{
    String serializedXmlDateTime = objectMapper.writeValueAsString(xmlDateTime);

    System.out.println(serializedXmlDateTime);
    // Serialized value: "2017-10-31T17:45:59.000+0000"
}
catch(IOException exception)
{
    exception.printStackTrace();
}

Unexpected deserialization:

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);

try
{
    XMLGregorianCalendar deserializedXmlDateTime2 = objectMapper.readValue(
        "\"2017-10-31T13:45:59-04:00\"", XMLGregorianCalendar.class);

    System.out.println(deserializedXmlDateTime2.toString());
    // Deserialized value: 2017-10-31T17:45:59.000Z
    // Expected value: 2017-10-31T13:45:59-04:00
}
catch(IOException exception)
{
    exception.printStackTrace();
}

I tried to debug the serialization a little, so I stepped through to CoreXMLSerializers.XMLGregorianCalendarSerializer._convert which calls XMLGregorianCalendar.toGregorianCalendar.

Then, in com.fasterxml.jackson.databind.ser.std.CalendarSerializer, there's this code:

    @Override
    protected long _timestamp(Calendar value) {
        return (value == null) ? 0L : value.getTimeInMillis();
    }

    @Override
    public void serialize(Calendar value, JsonGenerator g, SerializerProvider provider) throws IOException
    {
        if (_asTimestamp(provider)) {
            g.writeNumber(_timestamp(value));
            return;
        }
        _serializeAsString(value.getTime(), g, provider);
    }

If Jackson is configured to serialize as timestamp, it will call Calendar.getTimeInMillis(). If Jackson is configured to serialize as string, it will call Calendar.getTime(). However, java.util.Calendar.getTime does this:

   public final Date getTime() {
        return new Date(getTimeInMillis()); // no timezone!
    }

So Calendar serializer always uses Calendar.getTimeInMillis() and never considers the timezone, even if SerializationFeature.WRITE_DATES_AS_TIMESTAMPS is set to false. I didn't step through the deserialization but I'd expect the same problem there.

cowtowncoder commented 7 years ago

Thank you for reporting this. It seems unnecessary and wrong to go from Calendar to Date, since latter can not retain timezones (it only stores internal timestamp and requires timezone to be provided externally). I'll see how regular java.util.Calendar is handled.

cowtowncoder commented 7 years ago

Ah. Now I remember the rationale here was wrt deserialization... unfortunately this is a JDK (pre-Java-8) limitation: java.util.DateFormat only has parseDate() which returns java.util.Date, and as such has no means to return timezone that textual representation has. It does take it into account to get correct timestamp, but does not retain it. I played with it a bit few months back, trying to see if it was possible to access retained timezone value but that did not seem possible (it is converted to an offset, but I don't remember if even that was available).

So: I am not sure deserialization can be changed, the way things work wrt configurability (since custom formats are handled via DateFormat). Java 8 date/time (and Joda) do handle things much better, so if retaining of timezones is important it may be necessary to consider a change to date/time datatype used.

I will try to see what the issue is wrt serialization.

cowtowncoder commented 7 years ago

On serialization, it should be possible to change usage of DateFormat to set TimeZone at least in theory. Jackson internals do not do that since ability to dynamically change the value requires changes to mutability/sharing aspects... which is tricky, especially wrt trying to reproduce issues (or rather prove non-existence of them).

I will leave this open for serialization part at least: issue affects regular java.util.Calendar as well as XMLGregorianCalendar.

xinzheyang commented 3 years ago

@cowtowncoder please prioritize. this doesn't work with default typing https://stackoverflow.com/questions/49258540/jackson-polymorphic-serialization-generates-an-incorrect-class-name

when deserializing it will complain:

com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve type id 'java.util.GregorianCalendar' as a subtype of `javax.xml.datatype.XMLGregorianCalendar`: Not a subtype
cowtowncoder commented 3 years ago

@xinzheyang Not the same thing as problem reported here; please file a separate bug

xinzheyang commented 3 years ago

@cowtowncoder filed https://github.com/FasterXML/jackson-databind/issues/3217