0

I have a simple DTO that I would like to receive as a JSON and deserialize using Jackson's ObjectMapper

class SimpleDto {
    LocalDate date;
}

The problem is that the format of this date, however should be yyyy-MM-dd, is not fixed and it can miss some parts - e.g. it can be full date

{
    "date": "2020-01-01"
}

or just year with month without day

{
    "date": "2020-01"
}

or even just a year

{
    "date": "2020"
}

I'm trying to use ObjectMapper with JavaTimeModule registered but it does not support e.g. year without month and day format

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

is it possible to map it somehow to LocalDate? I saw some similar topics (like this) but seems that they're do not answer my question


EDIT

Now I'm starting to think should I treat this field as a LocalDate - as far as I understand there's nothing like LocalDate.of(2000, 0, 0) (and actually it's throwing exception) then deserializing 1990 to (what?) 1990-01-01 is obviously an error and could cause some exceptions in a logic that would assume that 01 is a valid and provided month and 01 is a valid and provided day

m.antkowicz
  • 13,268
  • 18
  • 37

3 Answers3

2

You can create your custom serializer having a DateTimeFormatter built using DateTimeFormatterBuilder#parseDefaulting.

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.util.Locale;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        //Test
        Stream.of(
                    "2020-01-01",
                    "2020-01",
                    "2020"
        ).forEach(s -> System.out.println(parseToLocalDate(s)));
    }
    static LocalDate parseToLocalDate(String date) {
        DateTimeFormatter dtf = new DateTimeFormatterBuilder()
                                    .appendPattern("u[-M[-d]]")
                                    .parseDefaulting(ChronoField.DAY_OF_MONTH, 1)
                                    .parseDefaulting(ChronoField.MONTH_OF_YEAR, 1)
                                    .toFormatter(Locale.ENGLISH);
        
        return LocalDate.parse(date, dtf);
    }
}

Output:

2020-01-01
2020-01-01
2020-01-01

The pattern, u[-M[-d]] has optional fields inside square brackets.

Arvind Kumar Avinash
  • 71,965
  • 6
  • 74
  • 110
1

How about extending LocalDateDeserializer and then use @JsonSerialize(using = YourLocalDateSerializer.class)

SoT
  • 898
  • 1
  • 15
  • 36
  • sounds like something that could work however I would like to avoid putting annotation over every `LocalDate` field - I have many of them – m.antkowicz Apr 23 '21 at 16:31
  • And all of these may come in these truncated formats? It’s best to limit the user of the custom deserializer to where it’s actually needed. – Ole V.V. Apr 24 '21 at 10:01
0

A LocalDate can hold only a valid date. I understand from your edit that you don’t want a date of January 1, 2020 in case only a year, 2020, was present in your JSON input. You may build a custom deserializer around the following code, but it will have some drawbacks.

public TemporalAccessor deserialize(String inputDate) {
    switch (inputDate.length()) {
    case 10:
        return LocalDate.parse(inputDate);
    case 7:
        return YearMonth.parse(inputDate);
    case 4:
        return Year.parse(inputDate);
    default:
        throw new IllegalArgumentException("Not a valid input date: " + inputDate);
    }
}

It will be unpractical that you don’t know the exact type of the result, manageable, though. And you will be sure that if the parsed value includes a month or a day of month, then those come from the input. For a demonstration:

    String[] exampleInputs = { "2020-01-01", "2020-01", "2020" };
    for (String example : exampleInputs) {
        TemporalAccessor parsed = deserialize(example);
        System.out.format(
                "%-10s parsed into %-10s, a %-9s. Has month? %-5b Has day? %b%n",
                example, parsed, parsed.getClass().getSimpleName(),
                parsed.isSupported(ChronoField.MONTH_OF_YEAR),
                parsed.isSupported(ChronoField.DAY_OF_MONTH));
    }

Output is:

2020-01-01 parsed into 2020-01-01, a LocalDate. Has month? true  Has day? true
2020-01    parsed into 2020-01   , a YearMonth. Has month? true  Has day? false
2020       parsed into 2020      , a Year     . Has month? false Has day? false
Ole V.V.
  • 81,772
  • 15
  • 137
  • 161