Skip to content

Accept sub-millisecond fractional seconds in xs:dateTime - #298

Merged
vpaturet merged 2 commits into
masterfrom
fix/subsecond-datetime-fraction
Sep 7, 2026
Merged

Accept sub-millisecond fractional seconds in xs:dateTime#298
vpaturet merged 2 commits into
masterfrom
fix/subsecond-datetime-fraction

Conversation

@vpaturet

@vpaturet vpaturet commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

xs:dateTime places no limit on the number of fractional-second digits, but the
parse formatter in LocalDateTimeISO8601XmlAdapter accepts at most three:

.appendFraction(ChronoField.MILLI_OF_SECOND, 0, 3, true)

A schema-valid timestamp carrying microseconds therefore fails to parse. Because
JAXB routes the resulting DateTimeParseException through the default
ValidationEventHandler, the property is simply left unset — so downstream
consumers see a silent null rather than an error.

This was hit in the wild by OpenTripPlanner on an Italian NeTEx dataset
(opentripplanner/OpenTripPlanner#7956), where a UicOperatingPeriod was
discarded because its FromDate was dropped:

<UicOperatingPeriod version="153" id="IT:ITH4:UicOperatingPeriod:U182_20260416_0">
  <FromDate>2026-05-03T23:30:09.398629+02:00</FromDate>
  <ToDate>2026-08-31T00:00:00</ToDate>
  <ValidDayBits>1111110111111011111101111110101111011111001111100111110011111001111100111110011111001111100111110011111001111100111110011111001</ValidDayBits>
</UicOperatingPeriod>

Note that the +02:00 offset is not the problem — the formatter already has an
optional offset section and discards it, which is the long-standing deliberate
behaviour discussed in #4. It is the six-digit fraction that fails.

The change

One line — widen the fraction to nanosecond precision:

-.optionalStart().appendFraction(ChronoField.MILLI_OF_SECOND, 0, 3, true).optionalEnd()
+.optionalStart().appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true).optionalEnd()

Both the field and the width have to change. Widening the width alone — as
proposed in #41 — makes parsing succeed but silently truncates, because
MILLI_OF_SECOND can only carry three digits:

input MILLI_OF_SECOND, 0, 9 (#41) NANO_OF_SECOND, 0, 9 (here)
...09.398629+02:00 ...09.398 (truncated) ...09.398629
...00.012345+01:00 ...00.012 (truncated) ...00.012345

Compatibility

Marshalled output is unchanged for every value that can round-trip today. The
fraction stays optional and minimal-width, so whole seconds still emit
2026-08-31T00:00:00 and milliseconds still emit ...09.398. Output differs
only for LocalDateTime values carrying sub-millisecond precision — which
currently cannot enter the model through unmarshalling at all, since they throw.
The wider format only preserves precision that is presently discarded.

There is no measurable parse cost; maxWidth bounds the digit loop rather than
adding work. If anything this is faster on the affected input, since it replaces
exception construction with a successful parse.

Prior discussion

Not addressed here

parseDefaulting(OFFSET_SECONDS, OffsetDateTime.now()...) is evaluated once at
class-initialisation, baking the JVM's startup offset into a static field, so a
long-running process that crosses a DST boundary keeps a stale offset. It is
also redundant, since the parse target is a LocalDateTime and the offset is
discarded either way.

xs:dateTime places no limit on the number of fractional-second digits,
but the parse formatter accepted at most three. A schema-valid timestamp
carrying microseconds threw a DateTimeParseException, which JAXB reports
to the default ValidationEventHandler and leaves the property unset - so
consumers saw a silent null.

Widen the fraction to NANO_OF_SECOND with a maximum of nine digits. Both
the field and the width have to change: widening the width alone leaves
MILLI_OF_SECOND unable to carry more than three digits, which parses
without error but silently truncates.

The fraction stays optional and minimal-width, so marshalled output is
unchanged for every value that can round-trip today.
@vpaturet
vpaturet force-pushed the fix/subsecond-datetime-fraction branch from c4835e5 to 42ee22d Compare September 7, 2026 12:57
@leonardehrenfried

Copy link
Copy Markdown
Contributor

This fixes the problem with the operating period mentioned in the description:

<UicOperatingPeriod version="153" id="IT:ITH4:UicOperatingPeriod:U182_20260416_0">
  <FromDate>2026-05-03T23:30:09.398629+02:00</FromDate>
  <ToDate>2026-08-31T00:00:00</ToDate>
  <ValidDayBits>1111110111111011111101111110101111011111001111100111110011111001111100111110011111001111100111110011111001111100111110011111001</ValidDayBits>
</UicOperatingPeriod>

@vpaturet
vpaturet marked this pull request as ready for review September 7, 2026 13:18
@leonardehrenfried

Copy link
Copy Markdown
Contributor

The xs:dateTime adapter was widened to nanosecond precision, but
LocalTimeISO8601XmlAdapter carried the identical defect: its parse
formatter accepted at most three fractional digits. Since bindings.xjb
routes every xs:time element through this adapter, a schema-valid
DepartureTime, ArrivalTime or WaitTime carrying microseconds threw a
DateTimeParseException and was left unset by the default
ValidationEventHandler - fields far more commonly populated than the
xs:dateTime ones that motivated the first fix. A producer emitting
microseconds in a date-time emits them in passing times too, so fixing
only xs:dateTime left the same data silently disappearing.

Widen the fraction to NANO_OF_SECOND with a maximum of nine digits, as
in LocalDateTimeISO8601XmlAdapter.

Marshalled output is unchanged for every value that can round-trip
today: the fraction stays optional and minimal-width. Caching is
unaffected, since the cache admits only times whose nano is zero.
@sonarqubecloud

sonarqubecloud Bot commented Sep 7, 2026

Copy link
Copy Markdown

@vpaturet
vpaturet merged commit 0a27ab6 into master Sep 7, 2026
4 checks passed
@vpaturet
vpaturet deleted the fix/subsecond-datetime-fraction branch September 7, 2026 13:38
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.

2 participants