Джексон: десериализация эпохи в LocalDate

У меня следующий JSON:

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

И класс POJO:

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

Я использую Джексона для десериализации:

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

Но это вызывает исключение:

com.fasterxml.jackson.databind.JsonMappingException: неожиданный токен (VALUE_NUMBER_INT), ожидаемый VALUE_STRING: ожидаемый массив или строка.

Как я могу десериализовать эпоху в LocalDate? Я хотел бы добавить, что если я изменю тип данных с LocalDate на java.util.Date, он будет работать отлично. Так что, возможно, лучше десериализовать в java.util.Date и создать геттер и сеттер, которые будут выполнять преобразование в / из LocalDate.


person kpater87    schedule 07.06.2017    source источник
comment
Имейте в виду, что преобразование миллисекунд в дату зависит от часового пояса. Например, число в вашем примере равно 1982-09-19T10: 54: 10.089Z, что, в свою очередь, равно 1982-09-18T23: 54: 10.089-11: 00 [Pacific / Midway].   -  person Ole V.V.    schedule 08.06.2017
comment
Избегайте старомодного класса Date. Единственное, что дает вам то, чего не делают современные классы, - это неприятности. Если вы хотите попробовать другие классы, кроме LocalDate, Instant - очевидный выбор.   -  person Ole V.V.    schedule 08.06.2017
comment
Вы искали до того, как спросить? Я думаю, вы можете найти вдохновение в этом вопросе: формат Java 8 LocalDate Jackson.   -  person Ole V.V.    schedule 08.06.2017
comment
@ Оле В.В. Да я искал. Я нашел аннотацию @JsonDeserialize(using = LocalDateDeserializer.class), но она не работает с эпохой. Тем не менее спасибо за подсказку.   -  person kpater87    schedule 08.06.2017


Ответы (3)


Мне удалось это сделать, написав собственный десериализатор (спасибо @Ole VV, чтобы указать мне на сообщение Формат Java 8 LocalDate Jackson):

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();
    }

}

Уведомление о часовом поясе также очень полезно. Спасибо!

Остается открытым вопрос, можно ли это сделать без написания собственного десериализатора?

person kpater87    schedule 08.06.2017
comment
Это предположение: я предполагаю, что если вы настаиваете на сериализации между LocalDate и миллисекундами, начиная с эпохи, вам понадобится собственный десериализатор. Если бы вы могли заменить длинные миллисекунды форматированной строкой даты или, вы бы согласились получить Instant вместо LocalDate, я был бы более оптимистичен в отношении использования встроенной десериализации. Сказал, едва коснувшись поверхности Джексона. Рад, что ты нашел выход. - person Ole V.V.; 08.06.2017

Другой вариант, который я выбрал, если у вас есть возможность изменить POJO, - просто объявить ваше поле как java.time.Instant.

public class FbProfile {
    long id;
    Instant birthday;
}

Это будет десериализоваться из ряда различных форматов, включая эпоху. Затем, если вам нужно использовать его как LocalDate или что-то еще в вашей бизнес-логике, просто сделайте то, что делают некоторые из перечисленных выше преобразователей:

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

or

LocalDateTime asDateTime = birthday.atZone(ZoneId.systemDefault()).toLocalDateTime()
person DeezCashews    schedule 08.06.2018

Я нашел способ сделать это без написания специального десериализатора, но он потребует некоторых изменений.

Во-первых, LocalDateDeserializer принимает пользовательский DateTimeFormatter. Итак, нам нужно создать средство форматирования, которое принимает миллисекунды эпох. Я сделал это, соединив поля INSTANT_SECONS и MILLI_OF_SECOND:

// 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);

Я также установил форматировщик с зоной UTC, чтобы на него не повлияли изменения часовых поясов и летнего времени.

Затем я создал десериализатор и зарегистрировался в моем 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);

Мне также пришлось удалить аннотацию из поля birthday (потому что аннотация, похоже, переопределяет конфигурацию модуля):

public class FbProfile {
    long id;

    // remove @JsonDeserialize annotation
    LocalDate birthday;
}

А теперь большая проблема: поскольку DateTimeFormatter принимает в качестве входных данных только String, а JSON содержит число в поле birthday, мне пришлось изменить JSON:

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

Обратите внимание, что я изменил birthday на String (поместите значение в кавычки).

При этом LocalDate правильно читается из JSON:

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

Примечания:

  • Я не смог найти способ передать номер непосредственно в форматтер (поскольку он принимает только String в качестве входных данных), поэтому мне пришлось изменить число на String. Если вы не хотите этого делать, вам все равно придется написать собственный конвертер.
  • Вы можете заменить ZoneOffset.UTC любым часовым поясом (даже ZoneId.systemDefault()), это будет зависеть от того, что нужно вашему приложению. Но, как сказано в комментарии @Ole VV, часовой пояс может вызвать изменение даты.
person Community    schedule 08.06.2017
comment
К сожалению, я не могу изменить структуру файла JSON. Тем не менее, спасибо за ваш ответ. - person kpater87; 08.06.2017
comment
Хорошо продумано и исследовано. - person Ole V.V.; 09.06.2017