问题
Our Rest API takes JSON input from several external parties. They all use "ISO-ish" formats, but the formatting of the time zone offset is slightly different. These are some of the most common formats we see:
2018-01-01T15:56:31.410Z
2018-01-01T15:56:31.41Z
2018-01-01T15:56:31Z
2018-01-01T15:56:31+00:00
2018-01-01T15:56:31+0000
2018-01-01T15:56:31+00
Our stack is Spring Boot 2.0 with Jackson ObjectMapper. In our data classes we use the type java.time.OffsetDateTime
a lot.
Several developers have tried to achieve a solution that parses all of the above formats, none have been successful. Particularly the fourth variant with a colon (00:00
) seems to be unparseable.
It would be great if the solution works without having to place an annotation on each and every date/time field of our models.
Dear community, do you have a solution?
回答1:
One alternative is to create a custom deserializer. First you annotate the respective field:
@JsonDeserialize(using = OffsetDateTimeDeserializer.class)
private OffsetDateTime date;
And then you create the deserializer. It uses a java.time.format.DateTimeFormatterBuilder, using lots of optional sections to deal with all the different types of offsets:
public class OffsetDateTimeDeserializer extends JsonDeserializer<OffsetDateTime> {
private DateTimeFormatter fmt = new DateTimeFormatterBuilder()
// date/time
.append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
// offset (hh:mm - "+00:00" when it's zero)
.optionalStart().appendOffset("+HH:MM", "+00:00").optionalEnd()
// offset (hhmm - "+0000" when it's zero)
.optionalStart().appendOffset("+HHMM", "+0000").optionalEnd()
// offset (hh - "+00" when it's zero)
.optionalStart().appendOffset("+HH", "+00").optionalEnd()
// offset (pattern "X" uses "Z" for zero offset)
.optionalStart().appendPattern("X").optionalEnd()
// create formatter
.toFormatter();
@Override
public OffsetDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return OffsetDateTime.parse(p.getText(), fmt);
}
}
I also used the built-in constant DateTimeFormatter.ISO_LOCAL_DATE_TIME because it takes care of the optional fraction of seconds - and the number of fractional digits seems to be variable as well, and this built-in formatter already takes care of those details for you.
I'm using JDK 1.8.0_144 and found a shorter (but not much) solution:
private DateTimeFormatter fmt = new DateTimeFormatterBuilder()
// date/time
.append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
// offset +00:00 or Z
.optionalStart().appendOffset("+HH:MM", "Z").optionalEnd()
// offset +0000, +00 or Z
.optionalStart().appendOffset("+HHmm", "Z").optionalEnd()
// create formatter
.toFormatter();
Another improvement you can make is change the formatter to be static final
, because this class is immutable and thread-safe.
回答2:
This is just about a quarter of an answer. I neither have experience with Kotlin nor Jackson, but I have a couple of solutions in Java that I’d like to contribute. I should be glad if you can fit them into a total solution somehow.
String modifiedEx = ex.replaceFirst("(\\d{2})(\\d{2})$", "$1:$2");
System.out.println(OffsetDateTime.parse(modifiedEx));
On my Java 9 (9.0.4) the one-arg OffsetDateTime.parse
parses all of your example strings except the one with offset +0000
without colon. So my hack is to insert that colon and then parse. The above parses all of your strings. It doesn’t work readily in Java 8 (there were some changes from Java 8 to Java 9).
The nicer solution that works in Java 8 too (I have tested):
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
.appendPattern("[XXX][XX][X]")
.toFormatter();
System.out.println(OffsetDateTime.parse(ex, formatter));
The patterns XXX
, XX
and X
match +00:00
, +0000
and +00
, respectively. We need to try them in order from the longest to the shortest to make sure that all text is being parsed in all cases.
回答3:
Thank you very much for all your input!
I chose the deserializer suggested by jeedas combined with the formatter suggested by Ole V.V (because it's shorter).
class DefensiveIsoOffsetDateTimeDeserializer : JsonDeserializer<OffsetDateTime>() {
private val formatter = DateTimeFormatterBuilder()
.append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
.appendPattern("[XXX][XX][X]")
.toFormatter()
override fun deserialize(p: JsonParser, ctxt: DeserializationContext)
= OffsetDateTime.parse(p.text, formatter)
override fun handledType() = OffsetDateTime::class.java
}
I also added a custom serializer to make sure we use the correct format when producing json:
class OffsetDateTimeSerializer: JsonSerializer<OffsetDateTime>() {
override fun serialize(
value: OffsetDateTime,
gen: JsonGenerator,
serializers: SerializerProvider
) = gen.writeString(value.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))
override fun handledType() = OffsetDateTime::class.java
}
Putting all the parts together, I added a @Configuraton
class to my spring classpath to make it work without any annotations on the data classes:
@Configuration
open class JacksonConfig {
@Bean
open fun jacksonCustomizer() = Jackson2ObjectMapperBuilderCustomizer {
it.deserializers(DefensiveIsoOffsetDateTimeDeserializer())
it.serializers(OffsetDateTimeSerializer())
}
}
来源:https://stackoverflow.com/questions/49390734/how-to-parse-different-iso-date-time-formats-with-jackson-and-java-time