Десериализовать формат даты JSON в ZonedDateTime с помощью objectMapper

Фон

  1. У меня есть следующий JSON (сообщение от Кафки)
{
      "markdownPercentage": 20,
      "currency": "SEK",
      "startDate": "2019-07-25"
}
  1. У меня есть следующее (сгенерированная схема JSON) POJO (я не могу изменить POJO, так как это общий ресурс в компании)
public class Markdown {
    @JsonProperty("markdownPercentage")
    @NotNull
    private Integer markdownPercentage = 0;
    @JsonProperty("currency")
    @NotNull
    private String currency = "";
    @JsonFormat(
        shape = Shape.STRING,
        pattern = "yyyy-MM-dd"
    )
    @JsonProperty("startDate")
    @NotNull
    private ZonedDateTime startDate;

    // Constructors, Getters, Setters etc.

}
  1. Наше приложение представляет собой приложение Spring Boot, которое читает сообщение JSON (1) от Kafka с помощью Spring Cloud Stream и использует POJO (2), а затем выполняет с ним какие-то действия.

Проблема

Когда приложение пытается десериализовать сообщение в объект, оно выдает следующее исключение

com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.time.ZonedDateTime` from String "2019-07-25": Failed to deserialize java.time.ZonedDateTime: (java.time.DateTimeException) Unable to obtain ZonedDateTime from TemporalAccessor: {},ISO resolved to 2019-07-25 of type java.time.format.Parsed
 at [Source: (String)"{"styleOption":"so2_GreyMelange_1563966403695_1361997740","markdowns":[{"markdownPercentage":20,"currency":"SEK","startDate":"2019-07-25"},{"markdownPercentage":20,"currency":"NOK","startDate":"2019-07-25"},{"markdownPercentage":20,"currency":"CHF","startDate":"2019-07-25"}]}"; line: 1, column: 126] (through reference chain: com.bestseller.generated.interfacecontracts.kafkamessages.pojos.markdownScheduled.MarkdownScheduled["markdowns"]->java.util.ArrayList[0]->com.bestseller.generated.interfacecontracts.kafkamessages.pojos.markdownScheduled.Markdown["startDate"])

    at com.fasterxml.jackson.databind.exc.InvalidFormatException.from(InvalidFormatException.java:67)
    at com.fasterxml.jackson.databind.DeserializationContext.weirdStringException(DeserializationContext.java:1549)
    at com.fasterxml.jackson.databind.DeserializationContext.handleWeirdStringValue(DeserializationContext.java:911)
    at com.fasterxml.jackson.datatype.jsr310.deser.JSR310DeserializerBase._handleDateTimeException(JSR310DeserializerBase.java:80)
    at com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer.deserialize(InstantDeserializer.java:212)
    at com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer.deserialize(InstantDeserializer.java:50)
    at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:127)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:288)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:286)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:245)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:27)
    at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:127)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:288)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4013)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3004)
    at com.bestseller.mps.functional.TestingConfiguration.test(TestingConfiguration.java:42)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: java.time.DateTimeException: Unable to obtain ZonedDateTime from TemporalAccessor: {},ISO resolved to 2019-07-25 of type java.time.format.Parsed
    at java.base/java.time.ZonedDateTime.from(ZonedDateTime.java:566)
    at com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer.deserialize(InstantDeserializer.java:207)
    ... 35 more
Caused by: java.time.DateTimeException: Unable to obtain ZoneId from TemporalAccessor: {},ISO resolved to 2019-07-25 of type java.time.format.Parsed
    at java.base/java.time.ZoneId.from(ZoneId.java:463)
    at java.base/java.time.ZonedDateTime.from(ZonedDateTime.java:554)
    ... 36 more

Текущий код

У меня определен следующий objectMapper

/**
     * Date mapper.
     *
     * @return the {@link ObjectMapper}
     */
    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd"));
        return mapper;
    }

Вопрос

Я понимаю, что полученному ZonedDateTime в POJO нужен элемент «время», которого нет в исходном сообщении. У меня есть контроль только над objectMapper. Есть ли возможная конфигурация, которая может заставить это работать?

Примечание

Я в порядке, если элемент времени в десериализованном POJO «предполагается» как startOfDay, то есть «00.00.00.000Z».

Вы имеют десериализуете прямо в этот тип? Местное свидание на самом деле совсем не то же самое, что ZonedDateTime. В качестве альтернативы, можете ли вы изменить свойство в Markdown на LocalDate? В конце концов, это то, что на самом деле представляют данные.

Jon Skeet 24.07.2019 14:01

Я не могу изменить POJO. Ни сообщения. Производители используют один и тот же POJO для создания сообщения.

Debapriyo Majumder 24.07.2019 14:03

Я предполагаю, что это работает для производителей, потому что @JsonFormat учитывается во время сериализации POJO в JSON Джексоном.

Debapriyo Majumder 24.07.2019 14:04

Поскольку startDate, который вы получаете из JSON, не имеет времени, я думаю, у вас не возникнет проблем с установкой времени на 00:00:00?

NickAth 24.07.2019 14:04

Правильный. В десериализованном POJO совершенно нормально, если элемент времени «предполагается» равным 00:00:00.000Z.

Debapriyo Majumder 24.07.2019 14:06
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
8
5
19 505
6
Перейти к ответу Данный вопрос помечен как решенный

Ответы 6

К сожалению, без изменения типа POJO на LocalDate это было бы сложно.

Ближайшее решение, которое я могу придумать, это написать кастомный JsonDeserializer для Джексона, что абсолютно не рекомендуется для такого рода вещей.

см. https://fasterxml.github.io/jackson-databind/javadoc/2.3.0/com/fasterxml/jackson/databind/JsonDeserializer.html

Ссылка мертва. Похоже на обновленный: fastxml.github.io/jackson-databind/javadoc/2.6/com/fasterx‌​ml/…

Chris R 22.03.2021 12:05

Вам нужно преобразовать переменную StringstartDate в ZonedDateTimne, после чего она будет преобразована и сохранена в вашей БД или где-то еще.

Поскольку в будущем json startDate имеет строковый формат, и вы сказали, что тогда вы не можете изменить POJO, вам нужно преобразовать его, прежде чем назначать его private ZonedDateTime startDate;

Вы можете сделать это, как в примере ниже:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss a z");
ZonedDateTime dateTime = ZonedDateTime.parse("2019-03-27 10:15:30 AM +05:30", formatter);
System.out.println(dateTime);

Вопрос был об использовании класса Jackson ObjectMapper для этого. Вы не отвечаете на вопрос.

allkenang 02.12.2020 17:01
Ответ принят как подходящий

I have control only over the ObjectMapper. Is there any possible configuration that can make this work?

Пока вас устраивают значения по умолчанию для времени и часового пояса, вы можете обойти это с помощью пользовательского десериализатора:

public class ZonedDateTimeDeserializer extends JsonDeserializer<ZonedDateTime> {

    @Override
    public ZonedDateTime deserialize(JsonParser jsonParser,
                                     DeserializationContext deserializationContext)
                                     throws IOException {

        LocalDate localDate = LocalDate.parse(
                jsonParser.getText(), 
                DateTimeFormatter.ISO_LOCAL_DATE);

        return localDate.atStartOfDay(ZoneOffset.UTC);
    }
}

Затем добавьте его в модуль и зарегистрируйте модуль в своем экземпляре ObjectMapper:

SimpleModule module = new SimpleModule();
module.addDeserializer(ZonedDateTime.class, new ZonedDateTimeDeserializer());

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);

Если добавление десериализатора в модуль вас не устраивает (в том смысле, что эта конфигурация будет применяться к другим экземплярам ZonedDateTime), вы можете положиться на миксины, чтобы определить, к каким полям будет применяться десериализатор. Сначала определите смешанный интерфейс, как показано ниже:

public interface MarkdownMixIn {

    @JsonDeserialize(using = ZonedDateTimeDeserializer.class)
    ZonedDateTime getDate();
}

А затем привяжите смешанный интерфейс к нужному классу:

ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(Markdown.class, MarkdownMixIn.class);

+1 за объяснение смешанного интерфейса, чтобы избежать проблем при десериализации других полей формата JSON «дата-время».

Debapriyo Majumder 25.07.2019 10:34

ты не хочешь использовать ISO_LOCAL_DATE_TIME ?

wilmol 02.10.2019 02:53

Вы можете написать свой собственный десериализатор, как показано в ответе @cassiomolin. Но также есть и другой вариант. В трассировке стека у нас есть метод DeserializationContext.weirdStringException, который позволяет нам предоставить нашему DeserializationProblemHandler обработку странных строковых значений. См. пример ниже:

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Shape;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.io.IOException;
import java.time.LocalDate;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.TimeZone;

public class AppJson {

    public static void main(String[] args) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        // override default time zone if needed
        mapper.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));

        mapper.registerModule(new JavaTimeModule());
        mapper.addHandler(new DeserializationProblemHandler() {
            @Override
            public Object handleWeirdStringValue(DeserializationContext ctxt, Class<?> targetType,
                String valueToConvert, String failureMsg) {
                LocalDate date = LocalDate.parse(valueToConvert, DateTimeFormatter.ISO_DATE);
                return date.atStartOfDay(ctxt.getTimeZone().toZoneId());
            }
        });
        String json = "{\"startDate\": \"2019-07-25\"}";
        Markdown markdown = mapper.readValue(json, Markdown.class);
        System.out.println(markdown);
    }
}

Над кодом печатается:

Markdown{startDate=2019-07-25T00:00-07:00[America/Los_Angeles]}

Проблема: я хотел бы анализировать даты из json в объекты java LocalDateTime/ZonedDateTime. ZonedDateTimeSerializer существует, но ZonedDateTimeDeserializer не существует. Поэтому я создал собственный ZonedDateTimeDeserializer.

  public static final String ZONED_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSz"; 

  @Getter
  @Setter
  @JsonSerialize(using = ZonedDateTimeSerializer.class)
  @JsonDeserialize(using = ZonedDateTimeDeserializer.class) // Doesn't exist, So I created a custom ZonedDateDeserializer utility class.
  @JsonFormat(pattern = ZONED_DATE_TIME_FORMAT)
  @JsonProperty("lastUpdated")
  private ZonedDateTime lastUpdated;

Решение: в итоге я получил более простой код с меньшим количеством строк.

Класс полезности для десериализация и ЗонедДатеВремя:

/**
 * Custom {@link ZonedDateTime} deserializer.
 *
 * @param jsonParser             for extracting the date in {@link String} format.
 * @param deserializationContext for the process of deserialization a single root-level value.
 * @return {@link ZonedDateTime} object of the date.
 * @throws IOException throws I/O exceptions.
 */
public class ZonedDateTimeDeserializer extends JsonDeserializer<ZonedDateTime> {

    @Override
    public ZonedDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
            throws IOException {

        return ZonedDateTime.parse(jsonParser.getText(), DateTimeFormatter.ofPattern(ZONED_DATE_TIME_FORMAT));
    }
}

Что если вместо этого вы используете Местная ДатаВремя. В этом случае все еще проще, нам уже предоставлены классы десериализатора и сериализатора. Нет необходимости в пользовательских служебных классах, как определено выше:

  @Getter
  @Setter
  @JsonSerialize(using = LocalDateSerializer.class)
  @JsonDeserialize(using = LocalDateTimeDeserializer.class)
  @JsonFormat(pattern = ZONED_DATE_TIME_FORMAT) //Specify the format you want: "yyyy-MM-dd'T'HH:mm:ss.SSS"
  @JsonProperty("created")
  private LocalDateTime created;

Другие ссылки, которые также помогли в исследовании: идеи, которые привели к этому решению

Ключевые слова: формат json localDateTime zonedDateTime

к сожалению, вы не можете десериализовать формат String Object в ZonedDateTime по умолчанию. Но вы можете решить эту проблему двумя способами.

путь 01

Чтобы изменить тип ZonedDateTime на LocalDate, введите свой класс POJO и передайте значение в виде строки 03-06-2012

путь 02

Но если вам нужно сохранить дату и время с часовым поясом, вам нужно сделать следующее, чтобы преодолеть

Шаг 01

Создайте класс для десериализации ZonedDateTime с помощью DateTimeFormat

public class ZonedDateTimeDeserializer extends JsonDeserializer<ZonedDateTime> {

    @Override
    public ZonedDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {

        DateTimeFormatter dateTimeFormatter=DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss z");
        LocalDate localDate = LocalDate.parse(p.getText(),dateTimeFormatter);

        return localDate.atStartOfDay(ZoneOffset.UTC);
    }
}

Шаг 02

Вы должны использовать этот класс Deserialize с затронутым полем в вашем классе POJO с поддержкой аннотации уровня метода @JsonDeserialize.

@JsonDeserialize(using = ZonedDateTimeDeserializer.class)
private ZonedDateTime startDate;

Шаг 03

Передача значения в виде строки в указанном выше формате, который задается в классе ZonedDateTimeDeserializer.

       "startDate" : "09-03-2003 10:15:00 Europe/Paris"

Другие вопросы по теме