Jackson: deserialize epoch to LocalDate - java

I have following JSON:
{
"id" : "1",
"birthday" : 401280850089
}
And POJO class:
public class FbProfile {
long id;
#JsonDeserialize(using = LocalDateDeserializer.class)
LocalDate birthday;
}
I am using Jackson to do deserialization:
public FbProfile loadFbProfile(File file) throws JsonParseException, JsonMappingException, IOException {
ObjectMapper mapper = new ObjectMapper();
FbProfile profile = mapper.readValue(file, FbProfile.class);
return profile;
}
But it throws an exception:
com.fasterxml.jackson.databind.JsonMappingException: Unexpected token
(VALUE_NUMBER_INT), expected VALUE_STRING: Expected array or string.
How can I deserialize epoch to LocalDate? I would like to add that if I change the datatype from LocalDate to java.util.Date it works perfectly fine. So maybe it's better to deserialize to java.util.Date and create the getter and setter which will do the conversion to/from LocalDate.

I've managed to do it writing my own deserializer (thank you #Ole V.V. to point me to the post Java 8 LocalDate Jackson format):
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();
}
}
Notice about timezone is also very useful. Thank you!
The still open question is if it can be done without writing own deserializer?

Another option that I went with if you have the ability to change the POJO, is to just declare your field as java.time.Instant.
public class FbProfile {
long id;
Instant birthday;
}
This will deserialize from a number of different formats including epoch. Then if you need to use it as a LocalDate or something else in your business logic, simply do what some of the converters above are doing:
LocalDate asDate = birthday.atZone(ZoneId.systemDefault()).toLocalDate()
or
LocalDateTime asDateTime = birthday.atZone(ZoneId.systemDefault()).toLocalDateTime()

I found a way to do it without writing a custom deserializer, but it'll require some modifications.
First, the LocalDateDeserializer accepts a custom DateTimeFormatter. So, we need to create a formatter that accepts an epoch millis. I did this by joining the INSTANT_SECONS and MILLI_OF_SECOND fields:
// 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);
I also set the formatter with UTC zone, so it won't be affected by timezones and DST changes.
Then, I've created the deserializer and registered in my 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);
I also had to remove the annotation from the birthday field (because the annotation seems to override the module configuration):
public class FbProfile {
long id;
// remove #JsonDeserialize annotation
LocalDate birthday;
}
And now the big issue: as the DateTimeFormatter accepts only String as input, and the JSON contains a number in birthday field, I had to change the JSON:
{
"id" : "1",
"birthday" : "401280850089"
}
Note that I changed birthday to a String (put the value between quotes).
With this, the LocalDate is read from JSON correctly:
FbProfile value = mapper.readValue(json, FbProfile.class);
System.out.println(value.getBirthday()); // 1982-09-19
Notes:
I couldn't find a way to pass the number directly to the formatter (as it takes only String as input), so I had to change the number to be a String. If you don't want to do that, then you'll have to write a custom converter anyway.
You can replace ZoneOffset.UTC with any timezone you want (even ZoneId.systemDefault()), it'll depend on what your application needs. But as told in #Ole V.V.'s comment, the timezone might cause the date to change.

Related

LocalDateTime parses invalid dates to valid date and does not throw any exception

I am using LocalDateTime in the request body of my API in Spring.
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-mm-dd HH:mm:ss")
#DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
#JsonDeserialize(using = LocalDateTimeDeserializer.class)
#JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime createdAt;
When I put an invalid date in request such as "2020-02-31 00:00:00" it is automatically converted to "2020-02-29 00:00:00". I want to throw Exception in case of an invalid date. It is mentioned in the official documentation that it converts to previous valid date .
In some cases, changing the specified field can cause the resulting date-time to become invalid,
such as changing the month from 31st January to February would make the day-of-month invalid.
In cases like this, the field is responsible for resolving the date.
Typically it will choose the previous valid date,
which would be the last valid day of February in this example.
You need to write a custom serializer for that.
class CustomLocalDateTimeSerializer extends StdSerializer<LocalDateTime> {
private static final DateTimeFormatter FORMATTER
= DateTimeFormatter.ofPattern("yyyy-mm-dd HH:mm:ss");
...
#Override
public void serialize(LocalDateTime value, JsonGenerator generator, SerializerProvider provider)
throws IOException, JsonProcessingException {
// Do your validation using FORMATTER.
// Serialize the value using generator and provider.
}
}
Then you can just use it in your annotation.
#JsonSerialize(using = CustomLocalDateTimeSerializer.class)
Note that DateTimeFormatter throws an exception when formatting/parsing invalid values.
Check out the source of LocalDateTimeSerializer to know what has to be done. Check out Jackson Date - 10. Serialize Java 8 Date Without Any Extra Dependency for examples of writing a custom serializer. This is done analogous for a custom deserializer.

Spring Boot change date format of request param field as DTO

My controller has a GET endpoint which accept lots of query parameters. So, instead of having multiple #QueryParam I created a CriteriaDTO in order to perform dynamic queries to our Mongo database
My controller endpoint :
#GetMapping
public List<MyObject> getAll(#Valid CriteriaDTO criteriaDTO){
return myObjectService.findAll(criteriaDTO);
}
public class CriteriaDTO {
private int offset = 0
private int limit = 20
#DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate minDate
// getters, setters ...
}
And, I want to pass the minDate is the URL with the following format yyyy-MM-dd but I need to convert it to the following format yyyy-MM-dd'T'HH:mm:ss.SSS.
My question is : Is there any annotation or something else which accepts the first format yyyy-MM-dd and automatically convert it to another ?
To be clear if I make the following call :
http://localhost:8080/api/myobject?minDate=2020-01-01
And then criteriaDTO.getminDate() will return 2020-01-01'T'00:00:00.000
Thanks for your help :)
You can do it in a more simple way than searching an annotation-magic solution.
Just add to your CriteriaDTO an additional getter for LocalDateTime:
public LocalDateTime getMinDateTime() {
return this.minDate.atTime(0, 0, 0, 0);
}
and use it wherever you need time instead of date.
Define setter and parse with SimpleDateFormat
public void setMinDate() {
if(!minDate.empty()) {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
this.minDate = formatter.parse(minDate)
}
}
I would recommend to use atStartOfDay instead of converting this 2020-01-01 to 2020-01-01'T'00:00:00.000 using custom deserializer. And also since you are manipulating the input data i would recommend to do it as separate operation
LocalDateTime date = criteriaDTO.getminDate().atStartOfDay() //2020-01-01'T'00:00
And you can also add formatter DateTimeFormatter to get required output
date.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) //2020-01-01'T'00:00:00
You have several options. Check what you exactly need,
LocalDate date = LocalDate.now();
LocalDateTime dateTime = LocalDateTime.of(date, LocalTime.MIDNIGHT);
System.out.println(dateTime); //2020-02-04T00:00:00
System.out.println(DateTimeFormatter.ISO_DATE_TIME.format(dateTime)); //2020-02-04T00:00:00
System.out.println(date.atStartOfDay()); ////2020-02-04T00:00
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS");
System.out.println(formatter.format(dateTime)); //2020-02-04T00:00:00.000
You need to modify the getter in dto to format it, for example:
class CriteriaDTO {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS");
private int offset = 0
private int limit = 20
#DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate minDate
public String getMinDate() {
return formatter.format(LocalDateTime.of(minDate, LocalTime.MIDNIGHT));
}
// getters, setters ...
}

Jackson - Can't deserialize datetime with timezone offset 'unparsed text found at index 23'

My datetime has to come from frontend with timezone offset: 2017-07-04T06:00:00.000+01:00
I cannot deserialize it with Jackson.
The error is:
Text '2017-07-04T06:00:00.000+01:00' could not be parsed, unparsed
text found at index 23;
I was trying to Google the solution by all are about DateTime with Z at the end.
#NotNull
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS aZ")
private LocalDateTime time;
Is there any solution for that?
The pattern a is used to parse AM/PM, which is not in the input String, that's why you get a parse error.
The input format matches an OffsetDateTime, which can be parsed with the respective built-in formatter DateTimeFormatter.ISO_OFFSET_DATE_TIME, so you can use this formatter in a deserializer object and register it in the module. You must also remove the JsonFormat annotation from the field.
ObjectMapper om = new ObjectMapper();
JavaTimeModule module = new JavaTimeModule();
LocalDateTimeDeserializer deserializer = new LocalDateTimeDeserializer(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
module.addDeserializer(LocalDateTime.class, deserializer);
om.registerModule(module);
This will parse the input and convert it to a LocalDateTime. In the test I've made, the value of LocalDateTime was set to 2017-07-04T06:00.
To control the output, you can either do:
om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
Which will output the LocalDateTime as 2017-07-04T06:00:00, or you can use a custom formatter:
LocalDateTimeSerializer serializer = new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS a"));
module.addSerializer(LocalDateTime.class, serializer);
The serializer above will output the field as 2017-07-04T06:00:00.000 AM. Please note that the Z pattern will not work because a LocalDateTime has no timezone information and it can't resolve its offset - because when you deserialized to a LocalDateTime, the offset information in the input (+01:00) was lost.
Another alternative (without the need to configure the object mapper) is to use the correct pattern in the annotation:
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS[xxx]")
private LocalDateTime time;
Note that I used the pattern [xxx] because the offset (+01:00) can be optional: when deserializing, this information is lost becase a LocalDateTime has no information about timezones and offsets, so when serializing this field won't be found - making the field optional (using [] delimiters) make it work for both deserialization and serialization.
This will deserialize the input 2017-07-04T06:00:00.000+01:00 and serialize to 2017-07-04T06:00:00.000 (note that the optional offset is not used in serialization, as the LocalDateTime has no such information).
If you want different formats for deserialization and serialization, you can also create custom classes and anotate them in the field:
public class CustomDeserializer extends LocalDateTimeDeserializer {
public CustomDeserializer() {
super(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
}
}
public class CustomSerializer extends LocalDateTimeSerializer {
public CustomSerializer() {
super(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS a"));
}
}
// in this case, don't use #JsonFormat
#JsonSerialize(using = CustomSerializer.class)
#JsonDeserialize(using = CustomDeserializer.class)
private LocalDateTime time;
This will use the format 2017-07-04T06:00:00.000+01:00 for deserialize and the format 2017-07-04T06:00:00.000 AM to serialize.

LocalDateTime not serialized as a timestamp

I'm using jackson-datatype-jsr310 to support Java 8 time data types.
I created a ObjectMapper like this:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false);
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
objectMapper.registerModule(new JavaTimeModule());
Foo foo = new Foo();
LocalDateTime t = LocalDateTime.now();
foo.setT1(Date.from(t.toInstant(ZoneOffset.UTC)));
foo.setT2(Timestamp.valueOf(t));
foo.setT3(t);
System.out.println(objectMapper.writeValueAsString(foo));
And parsed a LocalDateTime then I got [2016,12,21,15,53,57,178] not 1482306837178
Here is my test model class Foo
public class Foo {
private Date t1;
private Timestamp t2;
private LocalDateTime t3;
// Getters & Setters
}
I see the question Jackson Java 8 DateTime serialisation and the problem owner get a serialized string like 1421261297.356000000. I did't known how to get that. I'm using jackson 2.8.5 same with datatype-jsr310.
From the Jackson Modules Datetime documentation:
LocalDate, LocalTime, LocalDateTime, and OffsetTime, which cannot portably be converted to timestamps and are instead represented as arrays when WRITE_DATES_AS_TIMESTAMPS is enabled.
Full doc page here

Jackson date format with #JsonFormat?

I want to serialize certain Calendar fields of a POJO with a specific format.
with no annotations, fields like this:
private Calendar timestamp1;
private Calendar timestamp2;
produce JSON like this:
{ ..., timestamp1: 1402402106000, timestamp2: 1402488595000, ... }
I would to add a field formatted as a string as it actually represents a Day as a 24-hour unit, not a specific instant of time. But when I add a new field with an annotation:
#JsonFormat(pattern = "yyyy-MM-dd")
private Calendar oneDay;
I was hoping to get JSON like this:
{ ..., timestamp1: 1402402106000, timestamp2: 1402488595000, oneDay: "2014-06-12", ... }
Instead, I got a the following exception:
com.fasterxml.jackson.databind.JsonMappingException:
Cannot format given Object as a Date
(through reference chain: java.util.HashMap["data"]->java.util.ArrayList[0]-myPojo["oneDay"])
What am I doing wrong?
I'm using Jackson 2.2.0
Here's what I've used: #JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
That works for me.

Categories

Resources