Skip to content

Add JavaTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS (#76) - #386

Merged
cowtowncoder merged 4 commits into
FasterXML:2.xfrom
seonwooj0810:feat/issue-76-always-write-subsecond-digits
Aug 11, 2026
Merged

Add JavaTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS (#76)#386
cowtowncoder merged 4 commits into
FasterXML:2.xfrom
seonwooj0810:feat/issue-76-always-write-subsecond-digits

Conversation

@seonwooj0810

@seonwooj0810 seonwooj0810 commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Fixes #76.

Problem

The JDK ISO formatters omit the sub-second field entirely when it is zero, so the width of a serialized timestamp varies with its value:

Instant.parse("2017-09-14T04:28:48.100Z")  ->  "2017-09-14T04:28:48.100Z"
Instant.parse("2017-09-14T04:28:48.000Z")  ->  "2017-09-14T04:28:48Z"       // <-- no ".000"

OffsetDateTime is worse, since ISO_OFFSET_DATE_TIME also strips trailing zeroes within the fraction (.100 is written as .1). As reported in the thread, this breaks consumers that expect fixed-precision timestamps, and breaks lexicographic sorting of timestamps stored as text.

Change

Per @cowtowncoder's suggestion in the thread, the default behaviour is left alone and this adds an opt-in JavaTimeFeature:

new JavaTimeModule().enable(JavaTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS)

When enabled, at least 3 (millisecond) sub-second digits are always written, zero-padded:

value default feature enabled
2017-09-14T04:28:48Z 2017-09-14T04:28:48Z 2017-09-14T04:28:48.000Z
2017-09-14T04:28:48.100+02:00 2017-09-14T04:28:48.1+02:00 2017-09-14T04:28:48.100+02:00
2017-09-14T04:28:48.123456789Z 2017-09-14T04:28:48.123456789Z 2017-09-14T04:28:48.123456789Z

Applies to Instant, OffsetDateTime, ZonedDateTime (both with and without WRITE_DATES_WITH_ZONE_ID) and LocalDateTime. It affects the default format only — an explicit DateTimeFormatter, a @JsonFormat pattern, and numeric-timestamp serialization are all untouched.

Notes on the implementation

  • It does work with JDK-provided DateTimeFormatter, as you hoped — but not via DateTimeFormatterBuilder.appendInstant(3), which truncates: it renders ...48.123456789Z as ...48.123Z. The formatters in the new SubSecondFormatters therefore use appendFraction(NANO_OF_SECOND, 3, 9, true), i.e. minimum 3 digits, maximum 9, so nothing is lost.
  • The feature swaps the default formatter, not _formatter, since a non-null _formatter would also force serialization as a JSON String and override WRITE_DATES_AS_TIMESTAMPS. That required a small default-format-replacing copy constructor on InstantSerializerBase / LocalDateTimeSerializer, plus a withFeatures(JacksonFeatureSet<JavaTimeFeature>) on each of the four serializers, mirroring how the deserializers already receive module features.
  • LocalTime / OffsetTime are deliberately left out of this first cut: their ISO formats drop the seconds field too, which is a somewhat different question (cf. OffsetTimeSerializer not writing seconds for time object like 15:30:00Z (where seconds is 00) #299). Happy to fold them in if you'd like.
  • Naming: you floated PRESERVE_INSTANT_MSECS_ON_WRITE; I went with ALWAYS_WRITE_SUBSECOND_DIGITS since it also covers the non-Instant types and the ">= 3 digits" semantics. Trivial to rename if you prefer yours.

Verification done

  • New test AlwaysWriteSubSecondDigits76Test (9 cases). Each case asserts the default output and the feature-enabled output side by side, so the previous behaviour is pinned as well as the new. Covers: zero sub-second, higher precision not truncated, all four types, zone-id variant, numeric timestamps unaffected, explicit @JsonFormat pattern still wins, and a serialize/parse round-trip (including +10000-… and negative years).
  • Full build green on the 2.x branch: ./mvnw clean verifyTests run: 1129, Failures: 0, Errors: 0, Skipped: 1, BUILD SUCCESS.
  • Release notes (VERSION-2.x, CREDITS-2.x) updated; let me know if you'd rather handle those yourself.

Disclosure: this change was prepared with the assistance of an AI coding tool; the design, the appendInstant truncation finding and all test cases were reviewed and verified by me against a local build.

Default ISO serialization omits the sub-second field when it is zero, so
timestamp width varies with the value. Add an opt-in JavaTimeFeature that
always writes at least 3 (millisecond) sub-second digits for Instant,
OffsetDateTime, ZonedDateTime and LocalDateTime, while preserving higher
precision (up to 9 digits) so no information is truncated.

Signed-off-by: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com>
@cowtowncoder

Copy link
Copy Markdown
Member

Since implementation cannot be merged, I'd be +1 for only supporting 3.x for this functionality.

@JooHyukKim

Copy link
Copy Markdown
Member

FYI, if going for 3.x support, should directly open PR in Jackson-databind module

@seonwooj0810

Copy link
Copy Markdown
Contributor Author

Agreed — opened FasterXML/jackson-databind#6151 to add this as DateTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS on the 3.x side instead. Happy to close this PR whenever, or leave it as a reference for the port — your call.

@cowtowncoder

Copy link
Copy Markdown
Member

@seonwooj0810 Since both are done, let's consider them both. Thanks!

@cowtowncoder cowtowncoder changed the title Add JavaTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS (#76) Add JavaTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS (#76) Aug 11, 2026
@cowtowncoder cowtowncoder added this to the 2.23.0 milestone Aug 11, 2026
@cowtowncoder
cowtowncoder merged commit 261d6fc into FasterXML:2.x Aug 11, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-received Marker to denote that there is a CLA for pr

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Missing milliseconds when serializing Java 8 date-time if they are zeros (add JavaTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS)

3 participants