Deserialize JSON date format to ZonedDateTime using objectMapper
Asked Answered
V

6

9

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"

Vaishnava answered 24/7, 2019 at 11:57 Comment(5)
Do you have to deserialize straight to this type? A local date really isn't the same thing as a ZonedDateTime at all. Alternatively, can you change the property in Markdown to be a LocalDate? After all, that's what the data actually represents.Jacquejacquelin
I cannot change the POJO. Nor the message. Producers are using the same POJO to produce the message.Vaishnava
I assume it works for the producers coz the @JsonFormat is taken into account during POJO to JSON serialisation by Jackson.Vaishnava
Since the startDate you receive from the JSON has no time, I guess that you would not have any problem setting the time into 00:00:00 ?Decaliter
Correct. In the dezerialized POJO it is totally fine if the time element is "assumed" to be 00:00:00.000ZVaishnava
H
15

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);
Hydrocellulose answered 24/7, 2019 at 13:3 Comment(2)
+1 for explaining with the mix in interface in order to avoid issues while deserialising other JSON format "date-time" fields.Vaishnava
won't you want to use ISO_LOCAL_DATE_TIME ?Porter
W
4

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

Wireless answered 24/10, 2020 at 18:37 Comment(0)
E
1

unfortunately, you can not deserialize String Object to ZonedDateTime format by default. But you can overcome this problem in two ways.

way 01

To change ZonedDateTime type to LocalDate type in your POJO class and passing value as a String 03-06-2012

way 02

But If you have to store the date and time with timezone then you have to do the following way to overcome

Step 01

Create a class for ZonedDateTime Deserialization with 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);
    }
}

Step 02

You have to use that Deserialize class with the affected field in your POJO class with the support of @JsonDeserialize method level annotation.

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

Step 03

Passing the value as String in the above format which is given at the ZonedDateTimeDeserializer class

       "startDate" : "09-03-2003 10:15:00 Europe/Paris"
Ellingson answered 5/4, 2021 at 13:21 Comment(0)
X
0

Sadly, without changing the type of the POJO to LocalDate, that would be difficult.

The nearest solution that I can think of is to write a custom JsonDeserializer for Jackson, which is absolutely not a good practice for that kind of thing.

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

Xanthophyll answered 24/7, 2019 at 12:5 Comment(1)
Link is dead. This looks like an updated one: fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/…Shawnna
V
0

You can write your own deserialiser like is shown in @cassiomolin answer. But also there is another option. On stack trace we have DeserializationContext.weirdStringException method which allows us to provide our DeserializationProblemHandler with handling weird strings values. See below example:

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

Above code prints:

Markdown{startDate=2019-07-25T00:00-07:00[America/Los_Angeles]}
Viewpoint answered 24/7, 2019 at 14:2 Comment(0)
B
-1

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;

You can do so like the example below:

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);
Baryon answered 24/7, 2019 at 12:17 Comment(1)
The question was about using the Jackson ObjectMapper class to do this. You are not answering the question.Surrogate

© 2022 - 2025 — McMap. All rights reserved.