7

I have following JSON:

{
      "id" : "1",
      "birthday" : 401280850089
}

And POJO class:

public class FbProfile {
    long id;
    @JsonDeserialize(using = LocalDateDeserializer.class)
    LocalDate birthday;
}

I am using Jackson to do deserialization:

public FbProfile loadFbProfile(File file) throws JsonParseException, JsonMappingException, IOException {
    ObjectMapper mapper = new ObjectMapper();
    FbProfile profile = mapper.readValue(file, FbProfile.class);
    return profile;
}

But it throws an exception:

com.fasterxml.jackson.databind.JsonMappingException: Unexpected token (VALUE_NUMBER_INT), expected VALUE_STRING: Expected array or string.

How can I deserialize epoch to LocalDate? I would like to add that if I change the datatype from LocalDate to java.util.Date it works perfectly fine. So maybe it's better to deserialize to java.util.Date and create the getter and setter which will do the conversion to/from LocalDate.

kpater87
  • 1,190
  • 12
  • 31
  • 1
    Please be aware that converting milliseconds to date is time zone dependent. For example, the number in you example equals 1982-09-19T10:54:10.089Z, which in turn equals 1982-09-18T23:54:10.089-11:00[Pacific/Midway]. – Ole V.V. Jun 07 '17 at 23:18
  • 1
    Avoid the oldfashioned `Date` class. The only thing it gives you that modern classes don’t is trouble. If you want to try other classes than `LocalDate`, `Instant` is the obvious choice. – Ole V.V. Jun 07 '17 at 23:21
  • Did you search prior to asking? I am thinking you may find some inspiration in [this question: Java 8 LocalDate Jackson format](https://stackoverflow.com/questions/28802544/java-8-localdate-jackson-format). – Ole V.V. Jun 07 '17 at 23:26
  • 1
    @Ole V.V. Yes I searched. I found annotation `@JsonDeserialize(using = LocalDateDeserializer.class)` but it doesn't work with epoch. Nevertheless thank you for the hint. – kpater87 Jun 08 '17 at 07:59

3 Answers3

7

I've managed to do it writing my own deserializer (thank you @Ole V.V. to point me to the post Java 8 LocalDate Jackson format):

public class LocalDateTimeFromEpochDeserializer extends StdDeserializer<LocalDateTime> {

    private static final long serialVersionUID = 1L;

    protected LocalDateTimeFromEpochDeserializer() {
        super(LocalDate.class);
    }

    @Override
    public LocalDateTime deserialize(JsonParser jp, DeserializationContext ctxt)
            throws IOException, JsonProcessingException {
        return Instant.ofEpochMilli(jp.readValueAs(Long.class)).atZone(ZoneId.systemDefault()).toLocalDateTime();
    }

}

Notice about timezone is also very useful. Thank you!

The still open question is if it can be done without writing own deserializer?

kpater87
  • 1,190
  • 12
  • 31
  • 2
    This is a guess: my guess is if you insist on serializing between `LocalDate` and milliseconds since the Epoch, you will need your own deserializer. If you could substitute the long milliseconds with a formatted date string *or* you would accept getting an `Instant` instead of a `LocalDate`, I would be more optimistic about using the built-in deserialization. Said having barely scratched the surface of Jackson. Glad you found a way through. – Ole V.V. Jun 08 '17 at 11:33
3

Another option that I went with if you have the ability to change the POJO, is to just declare your field as java.time.Instant.

public class FbProfile {
    long id;
    Instant birthday;
}

This will deserialize from a number of different formats including epoch. Then if you need to use it as a LocalDate or something else in your business logic, simply do what some of the converters above are doing:

LocalDate asDate = birthday.atZone(ZoneId.systemDefault()).toLocalDate()

or

LocalDateTime asDateTime = birthday.atZone(ZoneId.systemDefault()).toLocalDateTime()
Andrew Wynham
  • 2,310
  • 21
  • 25
1

I found a way to do it without writing a custom deserializer, but it'll require some modifications.

First, the LocalDateDeserializer accepts a custom DateTimeFormatter. So, we need to create a formatter that accepts an epoch millis. I did this by joining the INSTANT_SECONS and MILLI_OF_SECOND fields:

// formatter that accepts an epoch millis value
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
    // epoch seconds
    .appendValue(ChronoField.INSTANT_SECONDS, 1, 19, SignStyle.NEVER)
    // milliseconds
    .appendValue(ChronoField.MILLI_OF_SECOND, 3)
    // create formatter, using UTC as timezone
    .toFormatter().withZone(ZoneOffset.UTC);

I also set the formatter with UTC zone, so it won't be affected by timezones and DST changes.

Then, I've created the deserializer and registered in my ObjectMapper:

ObjectMapper mapper = new ObjectMapper();
JavaTimeModule module = new JavaTimeModule();
// add the LocalDateDeserializer with the custom formatter
module.addDeserializer(LocalDate.class, new LocalDateDeserializer(formatter));
mapper.registerModule(module);

I also had to remove the annotation from the birthday field (because the annotation seems to override the module configuration):

public class FbProfile {
    long id;

    // remove @JsonDeserialize annotation
    LocalDate birthday;
}

And now the big issue: as the DateTimeFormatter accepts only String as input, and the JSON contains a number in birthday field, I had to change the JSON:

{
  "id" : "1",
  "birthday" : "401280850089"
}

Note that I changed birthday to a String (put the value between quotes).

With this, the LocalDate is read from JSON correctly:

FbProfile value = mapper.readValue(json, FbProfile.class);
System.out.println(value.getBirthday()); // 1982-09-19

Notes:

  • I couldn't find a way to pass the number directly to the formatter (as it takes only String as input), so I had to change the number to be a String. If you don't want to do that, then you'll have to write a custom converter anyway.
  • You can replace ZoneOffset.UTC with any timezone you want (even ZoneId.systemDefault()), it'll depend on what your application needs. But as told in @Ole V.V.'s comment, the timezone might cause the date to change.