Skip to content
Assay

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.

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:00Z
epoch = 1700000000
http_date = "Wed, 21 Oct 2026 07:28:00 GMT"
day = "2026-01-31"
either = 1700000000
Timestamps(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.

iso = "yesterday"
epoch = 1700000000
http_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 errors

Each 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.

@Validate(.after("2020-01-01")) var createdAt: Date
@Validate(.between("2020-01-01", "2030-01-01")) var effective: Date
created_at = 2019-06-01T00:00:00Z
effective = 2031-01-01T00:00:00Z
d.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 errors

Bounds 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.

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.