Assay
Dates
The documents here are TOML, the only format with date-times in its grammar. iso below is a
native value rather than a string.
Everything on this page behaves the same from JSON, YAML, XML or a property list. There a date is just text, and you write the declaration identically.
The formats
Section titled “The formats”import Foundation // a `Date` field needs it
@Schema(keys: .snakeCase, formats: .all)struct Timestamps: Equatable { var iso: Date // ISO-8601 by default @DateFormat(.unixSeconds) var epoch: Date @DateFormat(.rfc9110) var httpDate: Date @DateFormat(.pattern("yyyy-MM-dd")) var day: Date @DateFormat(.iso8601, .unixSeconds) var either: Date // a candidate chain}iso = 2026-09-11T12:00:00Zepoch = 1700000000http_date = "Wed, 21 Oct 2026 07:28:00 GMT"day = "2026-01-31"either = 1700000000Timestamps(iso: 2026-09-11 12:00:00 +0000, epoch: 2023-11-14 22:13:20 +0000, httpDate: 2026-10-21 07:28:00 +0000, day: 2026-01-31 00:00:00 +0000, either: 2023-11-14 22:13:20 +0000)Several formats in one attribute is a candidate chain: tried in order, first that parses wins. Reach for it when you are reading an API mid-migration, or a field that has never been consistent about it.
.unixMillis is there too.
When they do not parse
Section titled “When they do not parse”iso = "yesterday"epoch = 1700000000http_date = "Wed, 21 Oct 2026 07:28:00 GMT"day = "31/01/2026"either = "nope"d.toml: error: iso must be an ISO-8601 date — expected a 4-digit year
d.toml: error: day must be a date matching "yyyy-MM-dd" — expected a 4-digit year
d.toml: error: either must be an ISO-8601 date, or unix timestamp (seconds) — expected a 4-digit year
3 errorsEach failure names the field and shows you what was actually there. A chain reports once for the field rather than once per candidate, because four messages about one value is noise.
Rules about when
Section titled “Rules about when”@Validate(.after("2020-01-01")) var createdAt: Date@Validate(.between("2020-01-01", "2030-01-01")) var effective: Datecreated_at = 2019-06-01T00:00:00Zeffective = 2031-01-01T00:00:00Zd.toml:1:14: error: created_at must be after 2020-01-01 1 │ created_at = 2019-06-01T00:00:00Z │ ^^^^^^^^^^^^^^^^^^^^ 2 │ effective = 2031-01-01T00:00:00Z
d.toml:2:13: error: effective must be between 2020-01-01 and 2030-01-01 1 │ created_at = 2019-06-01T00:00:00Z 2 │ effective = 2031-01-01T00:00:00Z │ ^^^^^^^^^^^^^^^^^^^^
2 errorsBounds are ISO-8601 strings parsed once, at expansion, so a bound you typed wrong is a build error rather than a surprise once per document.
There is no .past or .future, deliberately. They need a clock, and a rule whose answer
depends on when you run it is a rule you cannot test.
No ICU, and no Foundation in the core
Section titled “No ICU, and no Foundation in the core”The parsers are arithmetic and return epoch seconds; the macro emits
Date(timeIntervalSince1970:) into your module. That keeps AssayCore free of Foundation,
which is why this works the same on Linux, Windows and WebAssembly — and why it measures
about 5.6× JSONDecoder with .iso8601.
.pattern is the one to watch if you are on a size budget. An arbitrary UTS-35 pattern needs
a real formatter, and on some platforms that means ICU.
- Enums — the other closed vocabulary.
- Dates, explained — candidate chains and the ICU note in full.