Skip to content

fix: support java.time.ZonedDateTime serialize/deserialize (#588) - #624

Merged
zwobill merged 1 commit into
arextest:mainfrom
DZQOX:fix/588-zoneddatetime-serializer
Aug 22, 2026
Merged

fix: support java.time.ZonedDateTime serialize/deserialize (#588)#624
zwobill merged 1 commit into
arextest:mainfrom
DZQOX:fix/588-zoneddatetime-serializer

Conversation

@DZQOX

@DZQOX DZQOX commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Fixes #588

What's wrong

Jackson is the default serializer of the agent, and it has no serializer registered for java.time.ZonedDateTime, so jackson-databind falls into its "unsupported JDK type" branch and throws on every response that contains a ZonedDateTime:

[[title=arex.serializer-serialize]] can not serialize object: org.springframework.http.ResponseEntity, serializer: null,
cause: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.ZonedDateTime`
not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling

The whole case can not be recorded, not just that one field. ZonedDateTime is a very common response field in Spring applications (RespResult#timestamp, audit fields of JHipster-style entities...).

Gson does not throw, but it has no adapter either, so it serializes the internal fields of ZonedDateTime instead of a time string, which is neither readable nor consistent with the other time types:

{"dateTime":"2026-08-22 22:58:53.285079907","offset":{"totalSeconds":28800},"zone":{"id":"Asia/Shanghai"}}

What this PR does

Adds ZonedDateTimeAdapter for both serializers, in the same style as the existing OffsetDateTimeAdapter / LocalDateTimeAdapter, so no new dependency (jackson-datatype-jsr310) is needed — as suggested by @lucas-myx in the issue.

Registered in all 5 serializers: JacksonSerializer, JacksonSerializerWithType, JacksonRequestSerializer, GsonSerializer, GsonRequestSerializer.

Serialized form keeps the zone id:

2020-06-09T09:00:00.000+08:00[Asia/Shanghai]

Three details worth reviewing:

  1. The zone id is kept ('['VV']'), instead of only the offset like OffsetDateTime. Otherwise ZoneId.of("Asia/Shanghai") comes back as the fixed offset +08:00 after the mock is deserialized, and application code that formats/queries the zone would behave differently during replay. The offset is still written, so a local time that appears twice in a DST overlap is restored to the correct instant.

  2. Nanosecond precision on jdk11+, the same as TimePatternConstants.localDateTimeFormat does for LocalDateTime, so that the round trip is lossless.

  3. The offset is ZZZZZ and the year is uuuu, which differs from the ZZZ/yyyy of the patterns used by the other time types. This is deliberate, the two shorter forms lose data here:

    • ZZZ renders +HHMM and drops the seconds of the offset. On deserialize the offset overrides the zone when the instant is computed, and the zone then re-derives the local time, so a zone that still had a sub-minute offset comes back shifted and the shift accumulates over record → replay → record. Africa/Monrovia was -00:44:30 until 1972, so plain Instant.EPOCH.atZone(...) drifts by 30s; ZoneOffset.ofTotalSeconds(8 * 3600 + 30) reproduces it on a present-day date.
    • yyyy is year-of-era with no era field in the pattern, so a year before 1 AD silently comes back as a positive one (-100101).

    Both are covered by the parameterized test.

Tests

  • JacksonSerializerTest#testZonedDateTime / GsonSerializerTest#testZonedDateTime: round trip + zone id is not degraded to an offset + the re-serialized json is identical.
  • JacksonSerializerTest#testZonedDateTimeSerializedFormat: pins the serialized bytes of both patterns literally, and asserts the jdk8/jdk11 branch stays bound to them. The serialized form is the contract between record and replay, a round-trip-only test would let a self-consistent format change ship silently, and CI never runs the tests on jdk8.
  • GsonSerializerTest#testZonedDateTimeSameFormatAsJackson: the two serializers must emit the same bytes, a case can be recorded with one and replayed with the other.
  • zonedDateTime added to TimeTestInfo / ZeroSecondTimeTestInfo, which covers the existing serialize/deserialize, with-type and request(denoising) test flows.

Known limitation, not introduced here

Mock data recorded on jdk8 can not be replayed on jdk11+ and vice versa, because the pattern switches between .SSS and .SSSSSSSSS and java.time parses S with an exact digit count. This is pre-existing behaviour shared with LocalDateTime and LocalTime, which switch the same way, so fixing it means giving all of them a variable width fraction (appendFraction(NANO_OF_SECOND, 0, 9, true)) and is left out of this PR.

@DZQOX
DZQOX requested a review from zwobill August 22, 2026 15:21
)

Jackson is the default serializer of the agent, and it throws
InvalidDefinitionException("Java 8 date/time type `java.time.ZonedDateTime`
not supported by default") for every response containing a ZonedDateTime,
so those cases can not be recorded at all. Gson had no adapter either and
fell back to reflection, writing the internal fields of ZonedDateTime
(dateTime/offset/zone) instead of a readable time string.

Add ZonedDateTimeAdapter for both serializers, as the other java.time types
already do, without introducing the jackson-datatype-jsr310 dependency.

The zone id is kept in the serialized value
(2020-06-09T09:00:00.000+08:00[Asia/Shanghai]) so that the deserialized
value keeps the original zone instead of degrading to a fixed offset, and
the offset still disambiguates the local time repeated in a DST overlap.
Same as LocalDateTime, nanosecond precision is used on jdk11+.

The offset is written as ZZZZZ and the year as uuuu instead of the ZZZ/yyyy
used by the patterns of the other types: ZZZ drops the seconds of the offset
and the offset overrides the zone when the instant is computed on
deserialize, so a zone that still had a sub-minute offset came back shifted
(Instant.EPOCH in Africa/Monrovia drifted 30s, and the shift accumulated
over record-replay-record), and yyyy is year-of-era, which turned a year
before 1 AD into a positive one.
@DZQOX
DZQOX force-pushed the fix/588-zoneddatetime-serializer branch from 7015bdb to bf523a6 Compare August 22, 2026 15:45
@sonarqubecloud

Copy link
Copy Markdown

@zwobill
zwobill merged commit 29cf9e7 into arextest:main Aug 22, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Java 8 date/time type java.time.ZonedDateTime not supported by default

2 participants