2

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

cheseaux
  • 5,187
  • 31
  • 50

1 Answers1

2

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.

  • Thank you for your thorough answer ! I'm aware of the alternative of using custom (de)serializers but I don't want to go that way. I also lied in my question, it was really printing `2017-09-21 09:25:32.1` and not `2017-09-21 11:25:32.1` but I'm aware of timezone issues so I faked it ;-) I think this might be a bug, I've opened an issue [here](https://github.com/FasterXML/jackson-dataformat-xml/issues/264). – cheseaux Sep 26 '17 at 13:57
  • @cheseaux Apparently, it seems to be a problem with `SimpleDateFormat` (which is used internally by jackson): https://stackoverflow.com/a/7160014/7605325 - I've just tested with plain Java (without jackson) and it has the same problem. Anyway, I've updated the answer, adding this info –  Sep 26 '17 at 14:02
  • @cheseaux I've opened an issue in Oracle and [they accepted](http://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8188772), which strongly suggests that the problem is really in `SimpleDateFormat` class (which is used internally by Jackson). Probably the best alternative in this case is to use the custom (de)serializers. –  Oct 04 '17 at 13:43