【问题标题】:Deserialize JSON date format to ZonedDateTime using objectMapper使用 objectMapper 将 JSON 日期格式反序列化为 ZonedDateTime
【发布时间】:2019-12-02 13:46:12
【问题描述】:

背景

  1. 我有以下 JSON(来自 Kafka 的消息)
{
      "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 应用程序,它使用 Spring Cloud Stream 从 Kafka 读取 JSON 消息 (1) 并使用 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;
    }

问题

我了解 POJO 中生成的 ZonedDateTime 需要一个源消息中不存在的“时间”元素。我只能控制 objectMapper。是否有任何可能的配置可以使这项工作?

注意

如果反序列化 POJO 中的时间元素“假定”为 startOfDay 即“00.00.00.000Z”,我很好

【问题讨论】:

  • 直接反序列化到这种类型吗?本地日期与ZonedDateTime 完全不同。或者,您能否将Markdown 中的属性更改为LocalDate?毕竟,这就是数据实际代表的内容。
  • 我无法更改 POJO。也不是消息。生产者使用相同的 POJO 来生成消息。
  • 我认为它适用于生产者,因为在杰克逊的 POJO 到 JSON 序列化过程中考虑了 @JsonFormat。
  • 由于您从 JSON 收到的 startDate 没有时间,我想您将时间设置为 00:00:00 不会有任何问题?
  • 正确。在 dezerialized POJO 中,如果“假定”时间元素为 00:00:00.000Z

标签: java json spring-boot jackson spring-cloud-stream


【解决方案1】:

我只能控制ObjectMapper。是否有任何可能的配置可以使这项工作?

只要您对时间和时区的默认值感到满意,您就可以使用自定义反序列化器来解决它:

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 实例),那么您可以依靠 mix-ins 来定义反序列化器将应用于哪些字段.首先定义一个mix-in接口,如下图:

public interface MarkdownMixIn {

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

然后将mix-in接口绑定到想要的类:

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

【讨论】:

  • +1 用于解释接口中的混合,以避免在反序列化其他 JSON 格式“日期时间”字段时出现问题。
  • 你不想使用ISO_LOCAL_DATE_TIME 吗?
【解决方案2】:

问题:我想将日期从 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;

解决方案:我最终得到了更简单且更少的代码行。

反序列化 ZonedDateTime的实用程序类:

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

如果您改用 LocalDateTime 会怎样。在这种情况下就更容易了,反序列化器和序列化器类都已经提供给我们了。不需要上面定义的自定义实用程序类:

  @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;

对研究也有帮助的其他链接: ideas that led to this solution

关键词:json格式localDateTime zonedDateTime

【讨论】:

    【解决方案3】:

    遗憾的是,如果不将 POJO 的类型更改为 LocalDate,这将很困难。

    我能想到的最接近的解决方案是为杰克逊写一个自定义的JsonDeserializer,这绝对不是那种事情的好习惯。

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

    【讨论】:

    【解决方案4】:

    您可以编写自己的反序列化器,如@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]}
    

    【讨论】:

      【解决方案5】:

      很遗憾,您不能默认将String Object 反序列化为ZonedDateTime 格式。但是您可以通过两种方式克服这个问题。

      01路

      POJO 类中将ZonedDateTime 类型更改为LocalDate 类型并将值作为字符串传递03-06-2012

      方式02

      但如果你必须用时区存储日期和时间,那么你必须采取以下方法来克服

      步骤 01

      使用 DateTimeFormat 为ZonedDateTime 反序列化创建一个类

      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

      @JsonDeserialize 方法级别注释的支持下,您必须在 POJO 类中使用带有受影响字段的 Deserialize 类。

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

      步骤 03

      以上述格式将值作为字符串传递给 ZonedDateTimeDeserializer 类

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

      【讨论】:

        【解决方案6】:

        您需要将您的String 变量startDate 转换为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 类来执行此操作。你没有回答问题。
        猜你喜欢
        • 2020-02-12
        • 1970-01-01
        • 2019-01-26
        • 2014-11-05
        • 1970-01-01
        • 1970-01-01
        • 2016-12-24
        • 1970-01-01
        • 2018-07-24
        相关资源
        最近更新 更多