如何使用 Jackson 将未分区的日期时间反序列化为分区

How to deserialize unzoned datetime to zoned using Jackson

我正在寻找将未分区的日期时间反序列化的最简单方法,例如将 2021-03-31 00:00:00 反序列化为具有静态定义时区的 OffsetDateTime。这些是来自第三方的 API 响应,它们不包括时区,但他们的文档声明所有时间戳都在特定区域中。

我认为 ObjectMapper.setTimeZone() 会起作用,但我没有任何运气。 Kotlin例子来说明:

data class Bar(
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    val timestamp: OffsetDateTime
)

@Test
fun `foo`() {
    val mapper = ObjectMapper()
        .configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, true)
        .registerModule(KotlinModule())
        .registerModule(JavaTimeModule())
        .setTimeZone(TimeZone.getTimeZone("America/Phoenix"))

    val serialized = "{\"timestamp\":\"2019-01-01 00:00:00\"}"

    val deserialized: Bar = mapper.readValue(serialized)
}

这会引发异常 Unable to obtain ZoneOffset from TemporalAccessor。将字段类型更改为 LocalDateTime 有效。杰克逊不应该使用提供的时区进行上下文反序列化吗?

我知道我可以编写自定义反序列化器,但我希望这种更简单的方法能够发挥作用。

我认为自定义反序列化器或更改为 LocaleDateTime 是唯一的选择...

com.fasterxml.jackson.datatype.jsr310.deser.InstantDesirializer的实现可以看出,mapper的时区并没有进入java.time类型的解析逻辑。

try {
    TemporalAccessor acc = _formatter.parse(string);
    value = parsedToValue.apply(acc); // creation of OffsetDateTime from the TemporarAccessor is done here
    if (shouldAdjustToContextTimezone(context)) {
        return adjust.apply(value, this.getZone(context));
    }
} catch (DateTimeException e) {
    value = _handleDateTimeException(context, e, string);
}

Jackson 的行为与以下内容类似:

val parsed: TemporalAccessor = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").parse("2019-01-01 00:00:00")
val result = OffsetDateTime.from(parsed) //Fails, but LocalDateTime.from(parsed) would have succeeded

//Time zone adjusting happens after that 

所以,我相信,您必须为此编写自定义反序列化程序:

data class Bar(
    @JsonDeserialize(using = OffsetDateTimeDeserializer::class)
    val timestamp: OffsetDateTime
)

class OffsetDateTimeDeserializer(vc: Class<*>? = null) : StdDeserializer<OffsetDateTime>(vc) {
    companion object {
        private val formatter: DateTimeFormatter = DateTimeFormatterBuilder()
            .appendPattern("yyyy-MM-dd HH:mm:ss")
            .parseDefaulting(
                ChronoField.OFFSET_SECONDS,
                //God bless Phoenix for not using DST
                TimeZone.getTimeZone("America/Phoenix").rawOffset.toLong() / 1000 //Convert from milliseconds to seconds
            )
            .toFormatter()
    }

    override fun deserialize(p: JsonParser, ctxt: DeserializationContext): OffsetDateTime =
        OffsetDateTime.parse(p.valueAsString, formatter)
}

从其他答案看来,您需要构建自定义反序列化器。下面是我将如何在 Java 中进行解析。然后我相信你会封装一个 deseializer 并翻译成 Kotlin。

我先声明:

private static final DateTimeFormatter formatter
        = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss");
private static final ZoneId thirdPartyZone = ZoneId.of("Brazil/Acre");

填写已知的具体时区。那么转换为:

    String valueAsString = "2021-03-31 00:00:00";

    OffsetDateTime odt = LocalDateTime.parse(valueAsString, formatter)
            .atZone(thirdPartyZone)
            .toOffsetDateTime();
    
    System.out.println(odt);

输出:

2021-03-31T00:00-05:00

此代码也适用于具有夏令时 (DST) 和其他时间异常的时区。

不要使用 TemporalAccessor。它的文档说:

This interface is a framework-level interface that should not be widely used in application code. Instead, applications should create and pass around instances of concrete types, such as LocalDate. There are many reasons for this, part of which is that implementations of this interface may be in calendar systems other than ISO. …

Link: TemporalAccessor documentation.