Jackson: parse custom offset date time

2019-03-20 12:43发布

问题:

I have a model which has a timestamp property:

class Model {
    @JsonProperty("timestamp")
    private OffsetDateTime timestamp;
}

The format of the timestamp is as following:

2017-09-17 13:45:42.710576+02

OffsetDateTime is unable to parse this:

com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type java.time.OffsetDateTime from String "2017-09-17 13:45:42.710576+02": Text '2017-09-17 13:45:42.710576+02' could not be parsed at index 10

How can I fix this?

回答1:

You must tell Jackson in what format the date is. Basically, you have year-month-day followed by hour:minute:second.microseconds and the offset with 2 digits (+02). So your pattern will be:

@JsonProperty("timestamp")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSSSSSx")
private OffsetDateTime timestamp;

Take a look at all the date/time patterns for a more detailed explanation.


If you want to preserve the same offset (+02) in the OffsetDateTime, don't forget to adjust the DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE option to false.

If this option is set to true (in my tests), the result is converted to UTC (but it actually converts to whatever timezone is configured in Jackson):

2017-09-17T11:45:42.710576Z

If I set to false, the offset used in the input is preserved:

2017-09-17T13:45:42.710576+02:00


The code above works with exactly 6 digits after decimal point. But if this quantity varies, you can use optional patterns, delimited by [].

Example: if the input can have 6 or 3 decimal digits, I can use pattern = "yyyy-MM-dd HH:mm:ss.[SSSSSS][SSS]x". The optional sections [SSSSSS] and [SSS] tells the parser to either consider 6 or 3 digits.

The problem with optional patterns is that, when serializing, it prints all the patterns (so it will print the fraction of second twice: with 6 and with 3 digits).


Another alternative is to create custom serializers and deserializers (by extending com.fasterxml.jackson.databind.JsonSerializer and com.fasterxml.jackson.databind.JsonDeserializer):

public class CustomDeserializer extends JsonDeserializer<OffsetDateTime> {

    private DateTimeFormatter formatter;

    public CustomDeserializer(DateTimeFormatter formatter) {
        this.formatter = formatter;
    }

    @Override
    public OffsetDateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException, JsonProcessingException {
        return OffsetDateTime.parse(parser.getText(), this.formatter);
    }
}

public class CustomSerializer extends JsonSerializer<OffsetDateTime> {

    private DateTimeFormatter formatter;

    public CustomSerializer(DateTimeFormatter formatter) {
        this.formatter = formatter;
    }

    @Override
    public void serialize(OffsetDateTime value, JsonGenerator gen, SerializerProvider provider) throws IOException, JsonProcessingException {
        gen.writeString(value.format(this.formatter));
    }
}

Then you can register those in the JavaTimeModule. How to configure this will depend on the environment you're using (example: in Spring you can configure in the xml files). I'll just do it programatically as an example.

First I create the formatter, using a java.time.format.DateTimeFormatterBuilder:

DateTimeFormatter formatter = new DateTimeFormatterBuilder()
    // date/time
    .appendPattern("yyyy-MM-dd HH:mm:ss")
    // optional fraction of seconds (from 0 to 9 digits)
    .optionalStart().appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true).optionalEnd()
    // offset
    .appendPattern("x")
    // create formatter
    .toFormatter();

This formatter accepts an optional fraction of second with 0 to 9 digits. Then I use the custom classes above and register them in the ObjectMapper:

// set formatter in the module and register in object mapper
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false);
JavaTimeModule module = new JavaTimeModule();
module.addSerializer(OffsetDateTime.class, new CustomSerializer(formatter));
module.addDeserializer(OffsetDateTime.class, new CustomDeserializer(formatter));
mapper.registerModule(module);

I also remove the @JsonFormat annotation from the field:

@JsonProperty("timestamp")
private OffsetDateTime timestamp;

And now it accepts values like 2017-09-17 13:45:42+02 (no fraction of seconds) and 2017-09-17 13:45:42.71014+02 (5 decimal digits). It can parse from 0 to 9 decimal digits (9 is the maximum supported by the API), and it prints exactly the same quantity when serializing.


The alternative above is very flexible as it allows to set the formatter in the custom classes. But it also sets the serialization and deserialization for all OffsetDateTime fields.

If you don't want that, you can also create a class with a fixed formatter:

static class CustomDeserializer extends JsonDeserializer<OffsetDateTime> {

    private DateTimeFormatter formatter = // create formatter as above

    // deserialize method is the same
}

static class CustomSerializer extends JsonSerializer<OffsetDateTime> {

    private DateTimeFormatter formatter = // create formatter as above

    // serialize method is the same
}

Then, you can add those to only the fields you want, using the annotations com.fasterxml.jackson.databind.annotation.JsonSerialize and com.fasterxml.jackson.databind.annotation.JsonDeserialize:

@JsonProperty("timestamp")
@JsonSerialize(using = CustomSerializer.class)
@JsonDeserialize(using = CustomDeserializer.class)
private OffsetDateTime timestamp;

With this, you don't need to register the custom serializers in the module, and only the annotated field will use the custom classes (the other OffsetDateTime fields will use the default settings).