Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deserialize JSON date format to ZonedDateTime using objectMapper

Background

  1. I have the following JSON (message from Kafka)
{
      "markdownPercentage": 20,
      "currency": "SEK",
      "startDate": "2019-07-25"
}
  1. I have the following (JSON schema generated) POJO (I cannot change the POJO as it is shared resource in the company)
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. Our application is a Spring Boot application which reads the JSON message (1) from Kafka using Spring Cloud Stream and uses the POJO (2) and then does stuff with it.

Problem

When the application tries to deserialize the message to the object it throws the following exception

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

Current Code

I have the following objectMapper defined

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

Question

I understand that the resulting ZonedDateTime in the POJO needs a 'time' element which is not present in the source message. I have control only over the objectMapper. Is there any possible configuration that can make this work ?

Note

I am fine if the time element in the deserialised POJO is "assumed" to be startOfDay i.e. "00.00.00.000Z"

like image 617
Debapriyo Majumder Avatar asked Jul 24 '19 11:07

Debapriyo Majumder


People also ask

How do I change the date format in ObjectMapper?

We can format a date using the setDateFormat() of ObjectMapper class. This method can be used for configuring the default DateFormat when serializing time values as Strings and deserializing from JSON Strings.

How does Jackson deserialize dates from JSON?

How to deserialize Date from JSON using Jackson. In order to correct deserialize a Date field, you need to do two things: 1) Create a custom deserializer by extending StdDeserializer<T> class and override its deserialize(JsonParser jsonparser, DeserializationContext context) method.

How do you deserialize a date?

This is how i'm deserializing the date: SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); getObjectMapper(). getDeserializationConfig(). setDateFormat(dateFormat);

What does ObjectMapper readValue do?

The simple readValue API of the ObjectMapper is a good entry point. We can use it to parse or deserialize JSON content into a Java object. Also, on the writing side, we can use the writeValue API to serialize any Java object as JSON output.

How to deserialize String object to zoneddatetime by default in Java?

unfortunately, you can not deserialize String Object to ZonedDateTime format by default. But you can overcome this problem in two ways. To change ZonedDateTime type to LocalDate type in your POJO class and passing value as a String 03-06-2012 But If you have to store the date and time with timezone then you have to do the following way to overcome

How do I use the customdatedeseralizer with objectmapper?

Once the deserializer is registered we must instruct ObjectMapper how we want to proceed if it hits a date. In Jackson 1.7, modules were introduced to extend jackson functionality so we can create a SimpleModule and register our CustomDateDeseralizer class. If you are using spring boot, it configures an ObjectMapper automatically for you.

Why can't I deserialize a JSON object using the person class?

If you try to use the Person class shown before to deserialize the corresponding JSON contents, you'll get an error. This is because we don't have an empty constructor that Jackson can use during its deserialization logic (using reflection).

How to change POJO StartDate to zoneddatetime in JSON?

You need to convert your String variable startDate to ZonedDateTimne then it will be converted and saved to your DB or whatever. As in coming json startDate is in string format and you have said that you can't change the POJO then, you need to convert it before assigning it to private ZonedDateTime startDate;


Video Answer


2 Answers

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

As long as you are happy with default values for the time and for the timezone, you could work around it with a custom deserializer:

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

Then add it to a module and register the module to your ObjectMapper instance:

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

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

If adding the deserializer to a module doesn't suit you (in the sense this configuration will be applied to other ZonedDateTime instances), then you could rely on mix-ins to define which fields the deserializer will be applied to. First define a mix-in interface, as shown below:

public interface MarkdownMixIn {

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

And then bind the mix-in interface to the desired class:

ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(Markdown.class, MarkdownMixIn.class);
like image 154
cassiomolin Avatar answered Oct 26 '22 13:10

cassiomolin


Problem: I would like to parse dates from json to java LocalDateTime/ZonedDateTime objects. ZonedDateTimeSerializer exists but the ZonedDateTimeDeserializer doesn't exist. Hence why I created a custom 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;

Solution: I ended up with simpler & fewer lines of code.

The Utility Class for deserialising the 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));
    }
}

What if you're using LocalDateTime instead. In that case it's even easier, both the deserializer & the serializer classes are already provided to us. No need for custom utility classes as defined above:

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

Other links that also helped with the research: ideas that led to this solution

Keywords: json format localDateTime zonedDateTime

like image 2
S34N Avatar answered Oct 26 '22 14:10

S34N