Testland
Browse all skills & agents

iso-8601-vs-rfc-3339-reference

Pure-reference catalog of the ISO 8601 vs RFC 3339 distinction. Covers the relationship (RFC 3339 is a strict subset of ISO 8601 designed for internet protocols), the syntactic differences (RFC 3339 disallows ISO 8601's '+02' offset short-form requires '+02:00'; RFC 3339 mandates a date-time separator T or space; ISO 8601 allows much more), the canonical date-time string format (YYYY-MM-DDTHH:MM:SS[.fff]±HH:MM or Z), per-language parser behaviour (Python isoformat, Java Instant.parse, JS Date.parse non-spec), and serialisation rules for APIs. Use when choosing a wire format, parsing third-party datetimes, or auditing time-string handling.

Install with skills.sh (any agent)

npx skills add testland/qa --skill iso-8601-vs-rfc-3339-reference
View source

iso-8601-vs-rfc-3339-reference

Overview

ISO 8601 and RFC 3339 are often used interchangeably but they are not the same. RFC 3339 is a stricter subset of ISO 8601 designed specifically for internet protocols. ISO 8601 has many optional variations (ordinal dates, week-dates, no separators, etc.) that RFC 3339 forbids.

Per RFC 3339 (opens in new window) §5.6: it "defines a date and time format for use in Internet protocols that is a profile of the ISO 8601 standard."

For APIs, always use RFC 3339. Parsers handle it predictably; ISO 8601 in full generality is parsing hell.

When to use

  • Picking a wire format for a new API.
  • Parsing a third-party datetime string of unclear provenance.
  • Auditing existing datetime handling.
  • Reviewing API specs / schemas.

The canonical format

Per RFC 3339:

2026-05-20T14:30:00Z                  # UTC (Z = +00:00)
2026-05-20T14:30:00.123456Z           # microsecond precision
2026-05-20T14:30:00+02:00             # CEST
2026-05-20T14:30:00-05:00             # EST
2026-05-20 14:30:00Z                  # space-separated (allowed)

Per RFC 3339:

  • Date format: YYYY-MM-DD (extended; no compact YYYYMMDD)
  • Date-time separator: T or space (T recommended)
  • Time-zone offset: Z or ±HH:MM (full form, with the colon)
  • Optional fractional seconds: .fff (any digits)

What RFC 3339 forbids that ISO 8601 allows

ISO 8601 validRFC 3339
20260520T143000Z (compact, no separators)Forbidden - needs hyphens + colons
2026-05-20T14:30:00+02 (offset short form)Forbidden - must be +02:00
2026-W21-3 (week date)Forbidden - week dates not supported
2026-140 (ordinal date)Forbidden - ordinal dates not supported
2026-05-20T14:30:00,123Z (comma decimal)Forbidden - period only
--05-20 (omitted year)Forbidden - year required
+002026-05-20T... (extended year)Forbidden - 4 digits
24:00:00 (midnight as end-of-day)Forbidden - only 00:00:00 (start)

Per-language parsers and anti-patterns

The per-language parser-support matrix (RFC 3339 / ISO 8601 tolerance), version notes for time-sensitive parser behaviour, and the full anti-pattern table are in references/parser-support.md.

Serialisation rules for APIs

RuleWhy
Always include time-zone offsetWithout it, the receiver guesses
Prefer UTC (Z) for storage / wireAvoids per-region drift
Use T separatorMore universally accepted
Include microseconds for distributed-systems useSubsecond resolution for ordering
Round-trip safely: parse + emit produces the same stringTest this; some libraries don't
For local-time semantics, emit offset (not zone name)+02:00 is portable; Europe/Berlin requires per-receiver zoneinfo

Common pitfalls

"Local time without offset"

2026-05-20T14:30:00

No Z, no +02:00. Ambiguous. Different libraries interpret differently:

  • Java Instant.parse → throws (offset required)
  • Python datetime.fromisoformat → returns naive datetime
  • JavaScript new Date(...) → assumes browser-local timezone

For wire format, always include offset.

Sortability

UTC strings (...Z) are lexically sortable. Mixed-offset strings are not - 2026-05-20T14:30:00+02:00 sorts before 2026-05-20T13:00:00Z even though it's later in time.

Always store UTC. Display local if needed.

Date-only

ISO 8601 allows 2026-05-20 (date without time). RFC 3339 §5.6 calls this "full-date" and allows it. But:

  • Some receivers parse it as 2026-05-20T00:00:00
  • Others as 2026-05-20T12:00:00 (noon to avoid timezone-flip surprises)

If you need date-only, document explicitly and test parsing.

Testable behaviours

BehaviourTest
Parser accepts all RFC 3339 formsT separator, space separator, microseconds, Z, +HH:MM
Parser rejects ISO-8601-only formsWeek date, ordinal date, compact T143000
Round-trip preserves precisionparse + serialise = original
Sortability holds for UTC stringsSort 1000 random UTC timestamps; verify lexical = chronological
API spec documents the formatOpenAPI uses format: date-time (RFC 3339)
Per-language client + server agreeCross-language test fixture

Limitations

  • RFC 3339 is silent on leap seconds in the wire format. Allows 60 in the seconds field; parsers vary on acceptance.
  • No native "date only" or "time only" in RFC 3339. ISO 8601 has both; receivers need both.
  • No durations or intervals. ISO 8601 has P1Y2M3D, RFC 3339 doesn't directly speak to these.
  • 2038 problem (32-bit time_t) is orthogonal but worth noting; affects parsing of timestamps near 2038-01-19.

References

Per-language parser support and anti-patterns

View source (opens in new window)

Per-language parser support and anti-patterns

Parser-support matrix, version notes, and the anti-pattern table extracted from the core reference. The canonical format, forbidden forms, serialisation rules, pitfalls, and testable behaviours stay in SKILL.md.

Per-language parser support

LanguageRFC 3339 strictISO 8601 fullTolerance
Python datetime.fromisoformatyespartialAccepts most RFC 3339
Python dateutil.parseryesmostly yesLenient
Java Instant.parseyes (RFC 3339 + Z)noStrict ISO 8601 subset
Java OffsetDateTime.parseyesmostly yesLenient
JavaScript Date.parseplatform-dependentNONon-spec; varies by browser
Rust chronoyespartialLenient
Go time.Parse(time.RFC3339, ...)yesnoStrict
.NET DateTimeOffset.Parseyesmostly yesLenient

JavaScript Date.parse is the worst. Per developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse (opens in new window): "implementation-specific... The exact behavior of this function varies between implementations." Use a library (date-fns, dayjs) or the Temporal proposal where available.

Version notes

  • Python datetime.fromisoformat: full RFC 3339 (including a trailing Z) only from 3.11; earlier versions reject Z.
  • Go: time.RFC3339 rejects fractional seconds; use time.RFC3339Nano for fractional.
  • Temporal is a proposal; availability varies by runtime.

Anti-patterns

Anti-patternWhy it failsFix
Date.parse('2026-05-20') in JSImplementation-dependent (browser-local? UTC?)Use a library; specify offset
Mix UTC and local-with-offset in same fieldSortability broken; consumer confusionPick one wire format
Skip the offset (2026-05-20T14:30:00)AmbiguousAlways offset
Storing local-format stringsLose tz info; can't reconstructStore UTC + zone name separately if local matters
4-digit milliseconds (2026...000)Not all parsers acceptUse 3 (milli) or 6 (micro) digits
Sub-second precision but UTC stringLose subsec on round-trip in some librariesTest the round-trip
Trusting Date.parse('5/20/2026')US format; ambiguousUse RFC 3339 always
Date-only without explicit timeReceiver-defined behaviourSpecify T00:00:00Z or document

Related skills

dotnet-faketime

Wraps .NET's TimeProvider abstraction (System.TimeProvider, introduced .NET 8) and FakeTimeProvider from Microsoft.Extensions.TimeProvider.Testing: SetUtcNow, Advance, AutoAdvanceAmount, CreateTimer, Delay, and the pre-.NET-8 ISystemClock migration path. Use when testing C# or F# code that reads the current time, uses timers, or awaits Task.Delay.

dst-transition-reference

Pure-reference catalog of Daylight Saving Time (DST) transition patterns and their canonical bug classes. Covers the spring-forward (skipped hour: 02:00 → 03:00 local) and fall-back (repeated hour: 02:00 → 01:00 local) transitions, the historical irregularity of DST (different jurisdictions, transitions on different dates, some regions abolish DST or never adopted it), the IANA timezone database (tz / Olson DB) as the canonical source, and the testable behaviors DST creates (duplicate / missing local timestamps, cron jobs that fire 0 or 2 times, billing periods that miss / double-count, recurring meetings on transition days). Per-jurisdiction DST-rule tables and refreshable per-region test-data fixtures live in references/. Use when designing or auditing time-handling code or test cases.

freezegun-python

Wraps freezegun (github.com/spulec/freezegun), the Python time-mocking library: @freeze_time decorator / context manager, freeze_time(...).start() + stop(), tick / move_to / tz_offset, and integration with datetime.now / time.time / time.localtime. Use when testing Python code that calls datetime / time.

jest-fake-timers

Wraps Jest's built-in modern fake-timers (built on @sinonjs/fake-timers since Jest 27): jest.useFakeTimers(), jest.setSystemTime(), jest.advanceTimersByTime(), jest.runAllTimers(), and jest.useRealTimers() for selective restoration. Use when testing JS/TS code in Jest where setTimeout / setInterval / Date / Date.now need deterministic control.

leap-second-reference

Pure-reference catalog of leap-second mechanics and the bugs they cause: the 23:59:60 UTC insertion (announced ~6 months ahead by IERS Bulletin C; 27 inserted 1972-2016; abolished by 2035 per CGPM 2022), the Google/AWS leap-smear alternative, and the four bug classes a real insertion exposes - time_t non-monotonicity, negative durations, NTP cascading, and cross-node clock skew - each with a monotonic-clock fix and a freezegun simulation. Use when auditing time-sensitive code (financial timestamping, distributed logs, NTP-driven schedulers) for second-by-second progress assumptions; for the far more common daylight-saving-time transition hazards, use dst-transition-reference instead.

libfaketime-c

Wraps libfaketime (github.com/wolfcw/libfaketime), the LD_PRELOAD library that fakes the clock for any binary by intercepting time() / gettimeofday() / clock_gettime(). Covers absolute-date mode (FAKETIME='2026-12-31 23:59:00'), relative offset (FAKETIME='-1d'), advance-rate (FAKETIME='@2026-12-31 23:59:00 x5' for 5x speed), per-process scope via LD_PRELOAD, and FAKETIME_NO_CACHE for high-resolution mocking. Use when you need to fake time, mock the clock, or freeze time for C/C++ or any native binary that needs deterministic wall-clock time.

mockclock-jvm

Wraps Java's java.time.Clock + InstantSource dependency-injection pattern for testing time-sensitive code. Covers Clock.fixed(instant, zone), Clock.offset(baseClock, duration), Clock.systemDefaultZone() for production, the InstantSource interface (Java 17+), and the recommended dependency-injection pattern (constructor-inject Clock instead of calling Instant.now() directly). Use when you need to wire the clock-injection pattern (Clock.fixed, Clock.offset, MutableClock, InstantSource, Spring @Bean) into JVM (Java / Kotlin / Scala) production or test code. For pure DST transition reference (skipped or repeated hours, IANA DB, cron-double-fire bug classes) without a clock-injection need, use dst-transition-reference instead.

sinon-fake-timers-js

Wraps Sinon's standalone @sinonjs/fake-timers library for JS/TS testing: install(), tick() / tickAsync(), setSystemTime(), restore(); covers timers (setTimeout / setInterval / requestAnimationFrame), Date / performance.now() / hrtime, and the toFake option for selective override. Runner-agnostic - drives the clock directly in Mocha, AVA, Jasmine, node:test, or the browser. Use when JS/TS code needs deterministic timer or clock control and the test runner does not already expose this library behind its own built-in fake-timer API.

timecop-ruby

Wraps timecop (github.com/travisjeffery/timecop), the Ruby time-mocking gem: Timecop.freeze, Timecop.travel, Timecop.scale (time-speedup), Timecop.return (cleanup), and RSpec-friendly helpers. Use when testing Ruby/Rails code that calls Time / Date / DateTime.

timezone-test-matrix-builder

Builds a timezone, daylight saving time (DST), and leap year / leap second test matrix from wherever a codebase reads or formats dates and times. Finds time-handling code (grep for datetime / Date / Instant / time.time / timezone), sorts each spot into storage, business-logic, display, cron, or billing, picks the edge cases that matter (DST spring-forward / fall-back, ambiguous local time, leap day Feb 29, ISO 8601 / RFC 3339 round-trip, zone-database updates), and emits per-spot test stubs wired to the language's fake-clock (mock-time) library. Use when a codebase needs timezone, DST, and leap-year test coverage derived from its own date/time usage.