cheseaux
cheseaux

Reputation: 5325

Jackson not parsing Timestamp correctly

I'm having trouble parsing java.sql.Timestamp from XML files using Jackson 2.8.5. Somehow the milliseconds are padded with zeros on the left side.

Here is a minimal example showing that :

public class Foo {

    @JacksonXmlProperty(localName = "ts")
    Timestamp ts;

    public static void main(String[] args) throws IOException {
        String xml = "<Foo><ts>2017-09-21T11:25:32.1Z</ts></Foo>"
        Foo foo = new XmlMapper().readValue(xml, Foo.class);
        System.out.println("foo.ts = " + foo.ts);
    }
}

foo.ts = 2017-09-21 11:25:32.001

Whereas if I parse the string manually, I get the expected value

System.out.println(Instant.parse("2017-09-21T11:25:32.1Z"));

2017-09-21 11:25:32.1

Upvotes: 2

Views: 1908

Answers (1)

user7605325
user7605325

Reputation:

That seems to be a problem with SimpleDateFormat (which is used internally by Jackson), as stated in this answer. I've also made a test with plain Java (without Jackson) and the error also occurs:

String s = "2017-09-21T11:25:32.1Z";
Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SX").parse(s);
System.out.println(new Timestamp(date.getTime())); // 2017-09-21 08:25:32.001

If you can change the xml, adding zeroes to the input (2017-09-21T11:25:32.100Z) works.


Another alternative is to write a custom deserializer for your field (using the proper methods in Timestamp class to convert from Instant):

public class CustomTimestampDeserializer extends JsonDeserializer<Timestamp> {

    @Override
    public Timestamp deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        return Timestamp.from(Instant.parse(p.getText()));
    }
}

Then you annotate the field to use this custom deserializer:

@JacksonXmlProperty(localName = "ts")
@JsonDeserialize(using = CustomTimestampDeserializer.class)
Timestamp ts;

Now, when printing the Timestamp, you get the correct value:

2017-09-21 08:25:32.1

Wait a minute, why the hour is 08? - That's because when you System.out.println a Timestamp, it implicity calls the toString() method, and this method converts the Timestamp to the JVM default timezone (in my case, it's America/Sao_Paulo, so it's correct, because 08:25 AM in São Paulo is equivalent to 11:25 AM in UTC). But the value kept by the Timestamp is correct.

You can read more about this behaviour of toString() in this article - it talks about java.util.Date, but the idea is the same (specially because Timestamp extends Date, so it has the same problems).


To serialize it back to xml, you can configure a custom serializer as well:

public class CustomTimestampSerializer extends JsonSerializer<Timestamp> {

    @Override
    public void serialize(Timestamp value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
        gen.writeString(value.toInstant().toString());
    }
}

Then you annotate the field to use this serializer:

@JacksonXmlProperty(localName = "ts")
@JsonDeserialize(using = CustomTimestampDeserializer.class)
@JsonSerialize(using = CustomTimestampSerializer.class)
Timestamp ts;

Just a detail: Instant.toString() will result in 2017-09-21T11:25:32.100Z. If you don't want those extra zeroes in the end, you can customize the format using a DateTimeFormatter. So the custom serializer will be like this:

public class CustomTimestampSerializer extends JsonSerializer<Timestamp> {

    private DateTimeFormatter fmt = new DateTimeFormatterBuilder()
        // date/time
        .appendPattern("yyyy-MM-dd'T'HH:mm:ss")
        // nanoseconds without leading zeroes (from 0 to 9 digits)
        .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
        // offset (Z)
        .appendOffsetId()
        // create formatter (set zone to UTC to properly format the Instant)
        .toFormatter().withZone(ZoneOffset.UTC);

    @Override
    public void serialize(Timestamp value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
        gen.writeString(fmt.format(value.toInstant()));
    }
}

This will print the timestamp as 2017-09-21T11:25:32.1Z.

Upvotes: 2

Related Questions