Prasath Govind
Prasath Govind

Reputation: 750

How to deserialize a Timestamp as-is using Jackson Annotation in Java?

I have a field in my Java Bean like below where @JsonFormat is a jackson annotation:

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm")
private Timestamp createdOn;

When I return the Timestamp, it is always getting converted to UTC time in my JSON response. Lets say on formatting my Timestamp , I get 2017-09-13 15:30 but my response is getting returned as 2017-09-13 10:00.

I know this is because of Jackson annotation by default takes the System TimeZone and converts the Timestamp to UTC. (In my case the server belongs to Asia/Calcutta time zone and thus the offset is +05:30 and the Jackson mapper subtracts 5 hrs and 30 minutes to convert the Timestamp to UTC)

Is there a way I return the Timestamp as-is, i.e, 2017-09-13 15:30 instead of 2017-09-13 10:00?

Upvotes: 2

Views: 11703

Answers (2)

user7605325
user7605325

Reputation:

I've made a test here, changing the JVM default timezone to Asia/Calcutta and creating a class with a java.sql.Timestamp field:

public class TestTimestamp {

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm")
    private Timestamp createdOn;

    // getter and setter
}

In your other question you told that JDK 8 is being used, so I tested in the same version (if you're using another version (JDK <= 7), check the "Not Java 8" section in the bottom). First I created a Timestamp that corresponds to September 13th 2017, at 10 AM in UTC:

TestTimestamp value = new TestTimestamp();
// timestamp corresponds to 2017-09-13T10:00:00 UTC
value.setCreatedOn(Timestamp.from(Instant.parse("2017-09-13T10:00:00Z")));

Then I serialized it with Jackson's ObjectMapper:

ObjectMapper om = new ObjectMapper();
String s = om.writeValueAsString(value);

The resulting String is:

{"createdOn":"2017-09-13 10:00"}

Note that, in the generated JSON, the output is in UTC (10 AM). If I deserialize this JSON:

value = om.readValue(s, TestTimestamp.class);
System.out.println(value.getCreatedOn());

This will print:

2017-09-13 15:30:00.0

That's because the Timestamp::toString() method (which is implicity called in System.out.println) prints the timestamp in the JVM default timezone. In this case, the default is Asia/Calcutta, and 10 AM in UTC is the same as 15:30 in Calcutta, so the output above is produced.

As I explained in my answer to your other question, a Timestamp object doesn't have any timezone information. It has just the number of nanoseconds since unix epoch (1970-01-01T00:00Z or "January 1st 1970 at midnight in UTC").

Using the example above, if you see the value of value.getCreatedOn().getTime(), you'll see that it's 1505296800000 - that's the number of milliseconds since epoch (to get the nanoseconds precision, there's the method getNanos()).

This milliseconds value corresponds to 10 AM in UTC, 7 AM in São Paulo, 11 AM in London, 7 PM in Tokyo, 15:30 in Calcutta and so on. You don't convert the Timestamp between zones, because the millis value is the same everywhere in the world.

What you can change, though, is the representation of this value (the corresponding date/time in a specific timezone).

In Jackson, you can create custom serializers and deserializers (by extending com.fasterxml.jackson.databind.JsonSerializer and com.fasterxml.jackson.databind.JsonDeserializer), so you can have more control over how you format and parse the dates.

First I create a serializer that formats the Timestamp to the JVM default timezone:

public class TimestampSerializer extends JsonSerializer<Timestamp> {

    private DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");

    @Override
    public void serialize(Timestamp value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
        // get the timestmap in the default timezone
        ZonedDateTime z = value.toInstant().atZone(ZoneId.systemDefault());
        String str = fmt.format(z);

        gen.writeString(str);
    }
}

Then I create a deserializer that reads the date in the JVM default timezone and creates the Timestamp:

public class TimestampDeserializer extends JsonDeserializer<Timestamp> {

    private DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");

    @Override
    public Timestamp deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        // parse to a LocalDateTime
        LocalDateTime dt = LocalDateTime.parse(jsonParser.getText(), fmt);
        // the date/time is in the default timezone
        return Timestamp.from(dt.atZone(ZoneId.systemDefault()).toInstant());
    }
}

I also change the field to use these custom classes:

// remove JsonFormat annotation
@JsonSerialize(using = TimestampSerializer.class)
@JsonDeserialize(using = TimestampDeserializer.class)
private Timestamp createdOn;

Now, when using the same Timestamp above (that corresponds to 2017-09-13T10:00:00Z). Serializing it produces:

{"createdOn":"2017-09-13 15:30"}

Note that now the output corresponds to the local time in the JVM default timezone (15:30 in Asia/Calcutta).

When deserializing this JSON, I get back the same Timestamp (corresponding to 10 AM in UTC).


This code uses the JVM default timezone, but it can be changed without notice, even at runtime, so it's better to always make it explicit which one you're using.

The API uses IANA timezones names (always in the format Region/City, like Asia/Calcutta or Europe/Berlin), so you can create them using ZoneId.of("Asia/Calcutta"). Avoid using the 3-letter abbreviations (like IST or PST) because they are ambiguous and not standard.

You can get a list of available timezones (and choose the one that fits best your system) by calling ZoneId.getAvailableZoneIds().

If you want to change the output to correspond to another timezone, just change ZoneId.systemDefault() to the zone you want. Just keep in mind that the same zone must be used to both serialize and deserialize, otherwise you'll get wrong results.


Not Java 8?

If you're using Java <= 7, you can use the ThreeTen Backport, a great backport for Java 8's new date/time classes.

The only difference is the package names (in Java 8 is java.time and in ThreeTen Backport is org.threeten.bp), but the classes and methods names are the same.

There's also another difference: only in Java 8 the Timestamp class has the methods toInstant() and from(), so you'll need to use org.threeten.bp.DateTimeUtils class to make the conversions:

public class TimestampSerializer extends JsonSerializer<Timestamp> {

    private DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");

    @Override
    public void serialize(Timestamp value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
        // get the timestmap in the default timezone
        ZonedDateTime z = DateTimeUtils.toInstant(value).atZone(ZoneId.systemDefault());
        String str = fmt.format(z);

        gen.writeString(str);
    }
}

public class TimestampDeserializer extends JsonDeserializer<Timestamp> {

    private DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");

    @Override
    public Timestamp deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        // parse to a LocalDateTime
        LocalDateTime dt = LocalDateTime.parse(jsonParser.getText(), fmt);
        // the date/time is in the default timezone
        return DateTimeUtils.toSqlTimestamp(dt.atZone(ZoneId.systemDefault()).toInstant());
    }
}

Upvotes: 6

Naman
Naman

Reputation: 31858

You can use timeZone to specify the timezone you want explicitly :

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm", timezone="Asia/Calcutta")
private Timestamp createdOn;

Upvotes: 2

Related Questions