Skip to content

feat(duration): add ISO 8601 / RFC 3339 duration format (duration-iso8601)#284

Merged
fredbi merged 1 commit into
go-openapi:masterfrom
fredbi:feat/duration-iso-support
Jul 14, 2026
Merged

feat(duration): add ISO 8601 / RFC 3339 duration format (duration-iso8601)#284
fredbi merged 1 commit into
go-openapi:masterfrom
fredbi:feat/duration-iso-support

Conversation

@fredbi

@fredbi fredbi commented Jul 14, 2026

Copy link
Copy Markdown
Member

Adds first-class support for the JSON Schema draft 2020-12 "duration" format (RFC 3339 Appendix A: P1Y2M3DT4H5M6S, P2W, ...) alongside the existing human-readable Duration, without breaking it.

Type & parser

  • New generic type ISODuration[P ISODurationPolicy] with phantom, zero-size policies DurationStrict / DurationLenient, and the strict alias DurationISO8601 = ISODuration[DurationStrict].
  • Single-pass, zero-allocation parser enforcing strict RFC 3339 App. A by default (component ordering & anchoring, W-exclusivity, ASCII digits, no fraction/sign/whitespace). Per-rule leniency is opt-in on the explicit ParseISO8601Duration(s, ...opts) path only -- never the registry/struct-field decode path, which must stay unambiguous.
  • Overflow handled with the stdlib/duration.go discipline: bound the integer part to 1<<63 before multiplying, then check after the fractional add, so the fraction path cannot silently wrap (MaxInt64/MinInt64 are exact).

Serialization

  • JSON, Text (policy-aware emit: a strict value errors rather than emit output a strict parser would reject; String stays lossless for display), SQL (Scan/Value as int64 nanoseconds), and BSON.
  • BSON is treated as a storage boundary (lossless via String + a lenient parse-back), so values a strict policy refuses to emit on the text/JSON path (a sign, sub-second precision) still round-trip through storage.

Registration

  • Registered under the distinct handle "duration-iso8601"; "duration" keeps mapping to the human-readable Duration (non-breaking).
  • Registered under the distinct handle "duration-human": the "human readable" (existing duration)
  • Modified the existing Default Registry so that "duration-human" remains the default mapping for "duration"
  • Added a new pre-built Registry (JSONSchema2020Registry) for which "duration-iso8601" becomes the default mapping for "duration"

Prerequisites:

  • Prerequisite refactor: MapStructureHookFunc now decodes via encoding.TextUnmarshaler (reflect.New + UnmarshalText), removing the hand-maintained decodeFormatFromString switch so new formats need no edits there and the json / mapstructure paths stop diverging.

Other additions:

  • conv.DurationISO8601 / conv.DurationISO8601Value helpers.

Tests & tooling

  • Strict parser passes all 36 cases of the vendored JSON Schema Test Suite (testdata/jsonschema-suite/duration.json); plus lenient/options/value- semantics/overflow, registry/SQL/BSON, and a fuzz corpus (temporal bucket, including overflow boundaries).
  • Postgres / MariaDB / Mongo (both the bsonlite and real-driver codecs) integration tests.
  • Benchmarks confirm zero-alloc parsing in the same order of magnitude as time.ParseDuration. Also fixes benchmarkIs to rotate its input set (b.Loop() keeps b.N constant during the loop, so the prior index pinned one element).
  • Add testable examples for the ISO 8601 duration format. This documents the new API surface introduced with the duration-iso8601 format as godoc-rendered runnable examples (external strfmt_test package):

Change type

Please select: 🆕 New feature or enhancement|🔧 Bug fix'|📃 Documentation update

Short description

Fixes

Full description

Checklist

  • I have signed all my commits with my name and email (see DCO. This does not require a PGP-signed commit
  • I have rebased and squashed my work, so only one commit remains
  • I have added tests to cover my changes.
  • I have properly enriched go doc comments in code.
  • I have properly documented any breaking change.

…8601)

* closes go-openapi#278

Adds first-class support for the JSON Schema draft 2020-12 "duration" format
(RFC 3339 Appendix A: P1Y2M3DT4H5M6S, P2W, ...) alongside the existing
human-readable Duration, without breaking it.

Type & parser
- New generic type ISODuration[P ISODurationPolicy] with phantom, zero-size
  policies DurationStrict / DurationLenient, and the strict alias
  DurationISO8601 = ISODuration[DurationStrict].
- Single-pass, zero-allocation parser enforcing strict RFC 3339 App. A by
  default (component ordering & anchoring, W-exclusivity, ASCII digits, no
  fraction/sign/whitespace). Per-rule leniency is opt-in on the explicit
  ParseISO8601Duration(s, ...opts) path only -- never the registry/struct-field
  decode path, which must stay unambiguous.
- Overflow handled with the stdlib/duration.go discipline: bound the integer
  part to 1<<63 before multiplying, then check after the fractional add, so
  the fraction path cannot silently wrap (MaxInt64/MinInt64 are exact).

Serialization
- JSON, Text (policy-aware emit: a strict value errors rather than emit output
  a strict parser would reject; String stays lossless for display), SQL
  (Scan/Value as int64 nanoseconds), and BSON.
- BSON is treated as a storage boundary (lossless via String + a lenient
  parse-back), so values a strict policy refuses to emit on the text/JSON path
  (a sign, sub-second precision) still round-trip through storage.

Registration
- Registered under the distinct handle "duration-iso8601"; "duration" keeps
  mapping to the human-readable Duration (non-breaking).
- Registered under the distinct handle "duration-human": the "human
  readable" (existing duration)
- Modified the existing Default Registry so that "duration-human"
  remains the default mapping for "duration"
- Added a new pre-built Registry (JSONSchema2020Registry) for which "duration-iso8601"
  becomes the default mapping for "duration"

Prerequisites:
- Prerequisite refactor: MapStructureHookFunc now decodes via
  encoding.TextUnmarshaler (reflect.New + UnmarshalText), removing the
  hand-maintained decodeFormatFromString switch so new formats need no edits
  there and the json / mapstructure paths stop diverging.

Other additions:
- conv.DurationISO8601 / conv.DurationISO8601Value helpers.

Tests & tooling
- Strict parser passes all 36 cases of the vendored JSON Schema Test Suite
  (testdata/jsonschema-suite/duration.json); plus lenient/options/value-
  semantics/overflow, registry/SQL/BSON, and a fuzz corpus (temporal bucket,
  including overflow boundaries).
- Postgres / MariaDB / Mongo (both the bsonlite and real-driver codecs)
  integration tests.
- Benchmarks confirm zero-alloc parsing in the same order of magnitude as
  time.ParseDuration. Also fixes benchmarkIs to rotate its input set (b.Loop()
  keeps b.N constant during the loop, so the prior index pinned one element).
- Add testable examples for the ISO 8601 duration format.
  This documents the new API surface introduced with the duration-iso8601 format
  as godoc-rendered runnable examples (external strfmt_test package):

Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.64368% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.22%. Comparing base (4de561f) to head (33ea953).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
duration_iso8601.go 92.64% 12 Missing and 10 partials ⚠️
mongo.go 57.14% 3 Missing and 3 partials ⚠️
format.go 88.23% 1 Missing and 1 partial ⚠️
register.go 96.72% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #284      +/-   ##
==========================================
+ Coverage   87.81%   88.22%   +0.41%     
==========================================
  Files          18       22       +4     
  Lines        2471     2778     +307     
==========================================
+ Hits         2170     2451     +281     
- Misses        208      221      +13     
- Partials       93      106      +13     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

This was referenced Jul 14, 2026
@fredbi
fredbi merged commit a9ba012 into go-openapi:master Jul 14, 2026
27 of 28 checks passed
@fredbi
fredbi deleted the feat/duration-iso-support branch July 14, 2026 11:25
@ccoVeille

Copy link
Copy Markdown
Contributor

Thank you

Comment thread README.md
## Durations

We have 2 very different definitions of the "duration" format: the "human-readable" duration that used to be just "duration",
and the new "duration-iso8601". There is no "dual" parser that accepts both formats: types are specialzed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo

Suggested change
and the new "duration-iso8601". There is no "dual" parser that accepts both formats: types are specialzed.
and the new "duration-iso8601". There is no "dual" parser that accepts both formats: types are specialized.

Comment thread duration_iso8601.go
Comment thread duration_iso8601.go
Comment thread duration_iso8601.go
Comment on lines +340 to +343
if neg {
return -time.Duration(total), nil
}
return time.Duration(total), nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry Christophe I lost track of your comment there. What is wrong with this expression ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's OK, you replied here

#284 (comment)

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.

ISO 8601 Durations

2 participants