std::chrono Support¶
Glaze provides first-class support for std::chrono types, enabling seamless serialization and deserialization of durations and time points.
Supported Types¶
Durations¶
All std::chrono::duration types are supported and serialize as their numeric count value:
std::chrono::milliseconds ms{12345};
std::string json = glz::write_json(ms).value(); // "12345"
std::chrono::milliseconds parsed{};
glz::read_json(parsed, json); // parsed.count() == 12345
This works with any duration type, including custom periods:
std::chrono::seconds s{3600}; // "3600"
std::chrono::nanoseconds ns{123456789}; // "123456789"
std::chrono::hours h{24}; // "24"
// Custom period (e.g., 60fps frames)
using frames = std::chrono::duration<int64_t, std::ratio<1, 60>>;
frames f{120}; // "120"
// Floating-point rep
std::chrono::duration<double, std::milli> ms{123.456}; // "123.456"
System Clock Time Points¶
std::chrono::system_clock::time_point serializes as an ISO 8601 string in UTC:
auto now = std::chrono::system_clock::now();
std::string json = glz::write_json(now).value();
// "2024-12-13T15:30:45.123456789Z"
The precision of fractional seconds matches the time point's duration type:
using namespace std::chrono;
sys_time<seconds> tp_sec{...}; // "2024-12-13T15:30:45Z"
sys_time<milliseconds> tp_ms{...}; // "2024-12-13T15:30:45.123Z"
sys_time<microseconds> tp_us{...}; // "2024-12-13T15:30:45.123456Z"
sys_time<nanoseconds> tp_ns{...}; // "2024-12-13T15:30:45.123456789Z"
Parsing supports multiple ISO 8601 formats:
std::chrono::system_clock::time_point tp;
// UTC with Z suffix
glz::read_json(tp, "\"2024-12-13T15:30:45Z\"");
// With timezone offset
glz::read_json(tp, "\"2024-12-13T15:30:45+05:00\"");
glz::read_json(tp, "\"2024-12-13T15:30:45-08:00\"");
// With fractional seconds
glz::read_json(tp, "\"2024-12-13T15:30:45.123456789Z\"");
Date-Only Time Points (sys_days)¶
Breaking change:
std::chrono::sys_dayspreviously serialized as the full ISO 8601 datetime"YYYY-MM-DDT00:00:00Z". It now serializes as the date-only string"YYYY-MM-DD". The reader accepts both forms, so consumers upgrading Glaze on the read side stay compatible; producers that emit the new shape break older readers that strictly require a time component.
Time points whose duration period is exactly days (std::chrono::sys_days, i.e. std::chrono::sys_time<std::chrono::days>) serialize as date-only strings, since the time-of-day at that precision is always zero:
using namespace std::chrono;
sys_days d = year{2024} / month{6} / day{15};
std::string json = glz::write_json(d).value(); // "2024-06-15"
sys_days parsed{};
glz::read_json(parsed, "\"2024-06-15\"");
On read, both the bare date form and a full ISO 8601 datetime are accepted; a full datetime is floored to days precision after any timezone offset is applied to UTC:
sys_days parsed{};
glz::read_json(parsed, "\"2024-06-15T15:30:45Z\""); // parsed == 2024-06-15
glz::read_json(parsed, "\"2024-06-14T20:00:00-08:00\""); // parsed == 2024-06-15 (UTC)
Footgun: a sender that thinks "June 14 in California" and emits "2024-06-14T23:00:00-08:00" (= 2024-06-15 07:00:00Z) gets 2024-06-15 back, not 2024-06-14. The date is the UTC date of the instant, not the local date in the offset. If your protocol means "the local civil date," send a bare "YYYY-MM-DD" instead of a datetime with offset.
The reader rejects negative-year strings ("-0001-01-01") and 5+ digit years for symmetry with the writer's [0000, 9999] range.
std::chrono::year_month_day¶
std::chrono::year_month_day serializes as a "YYYY-MM-DD" JSON string:
using namespace std::chrono;
year_month_day ymd{year{2024}, month{6}, day{15}};
std::string json = glz::write_json(ymd).value(); // "2024-06-15"
year_month_day parsed{};
glz::read_json(parsed, json);
Unlike sys_days, year_month_day accepts only the bare date form on read; embed a sys_days field if you also need to accept full datetimes.
Years must lie within RFC 3339's four-digit range [0000, 9999] on write; values outside that range fail to serialize with error_code::constraint_violated rather than emit wrap-around digits.
Steady Clock Time Points¶
std::chrono::steady_clock::time_point serializes as a numeric count (time since epoch), since steady clock's epoch is implementation-defined and not meaningful as a calendar time:
auto start = std::chrono::steady_clock::now();
std::string json = glz::write_json(start).value(); // numeric count
std::chrono::steady_clock::time_point parsed;
glz::read_json(parsed, json);
// Exact roundtrip - no precision loss
Epoch Time Wrappers¶
For APIs that expect Unix timestamps (numeric epoch time) instead of ISO 8601 strings, Glaze provides epoch_time<Duration> wrappers:
#include "glaze/glaze.hpp"
// Convenience aliases
glz::epoch_seconds // Unix timestamp in seconds
glz::epoch_millis // Unix timestamp in milliseconds
glz::epoch_micros // Unix timestamp in microseconds
glz::epoch_nanos // Unix timestamp in nanoseconds
Usage¶
glz::epoch_seconds ts;
ts.value = std::chrono::system_clock::now();
std::string json = glz::write_json(ts).value(); // "1702481400"
glz::epoch_seconds parsed;
glz::read_json(parsed, json);
Implicit Conversion¶
epoch_time wrappers implicitly convert to/from system_clock::time_point:
glz::epoch_millis ts = std::chrono::system_clock::now();
std::chrono::system_clock::time_point tp = ts;
In Structs¶
Use epoch wrappers when your API requires numeric timestamps:
struct ApiResponse {
std::string data;
glz::epoch_millis created_at; // Serializes as: 1702481400123
glz::epoch_millis updated_at;
};
struct Event {
std::string name;
std::chrono::system_clock::time_point timestamp; // ISO 8601 string
std::chrono::milliseconds duration; // Numeric count
};
Per-Field Format Customization¶
The clock type alone decides the default representation (ISO 8601 for system_clock, numeric count for steady_clock, and so on). When a single field needs a different shape, the per-field wrappers below decouple the format from the type. They are applied inside glz::object(...) within a glz::meta<T> specialization and require #include "glaze/chrono.hpp".
glz::date_format — custom strftime-subset pattern¶
glz::date_format(&T::member, "pattern") serializes a system_clock time point (or a year_month_day) using a strftime-style pattern instead of ISO 8601:
#include "glaze/chrono.hpp"
struct Event {
std::chrono::system_clock::time_point start{};
std::chrono::sys_days day{};
};
template <>
struct glz::meta<Event> {
using T = Event;
static constexpr auto value = glz::object(
"start", glz::date_format(&T::start, "%Y-%m-%d %H:%M:%S"), // "2026-06-18 12:34:56"
"day", glz::date_format(&T::day, "%Y/%m/%d")); // "2026/06/18"
};
The wrapper is bidirectional: the same pattern parses the value back on read.
Event e{};
e.start = std::chrono::sys_days{std::chrono::year{2026} / 6 / 18} + std::chrono::hours{12} +
std::chrono::minutes{34} + std::chrono::seconds{56};
std::string json = glz::write_json(e).value(); // {"start":"2026-06-18 12:34:56","day":"..."}
Event parsed{};
auto ec = glz::read_json(parsed, json); // parsed.start == e.start
The member pointer and pattern are passed as ordinary arguments (not template parameters), so fields that share a member type but differ in format do not each spin up a fresh set of template instantiations. The pattern is still a compile-time literal and is validated at compile time.
Supported conversion specifiers (locale-independent by design):
| Token | Meaning | Token | Meaning |
|---|---|---|---|
%Y |
year (4 digits) | %H |
hour, 24-hour (2) |
%m |
month (2 digits) | %M |
minute (2 digits) |
%d |
day (2 digits) | %S |
second (2 digits) |
%F |
%Y-%m-%d |
%T |
%H:%M:%S |
%% |
literal % |
The pattern is validated at compile time:
- An unsupported token (e.g.
%A,%j,%z) is a hard compile error. - The pattern must contain a full calendar date (
%Y %m %d, or%F); a time-only pattern cannot reconstruct an absolute time point and is rejected. - A
year_month_dayfield rejects time tokens (%H %M %S %T).
Notes and limitations (MVP scope):
- Times are treated as UTC wall-clock. There is no timezone token; the decomposed fields are UTC, matching the ISO 8601 writer.
- The four-digit-year range still applies. As with the ISO 8601 writers, a year outside
[0000, 9999](or a non-ok()year_month_day) fails to serialize witherror_code::constraint_violatedrather than emitting wrap-around digits. %Swrites integer seconds only. Sub-second precision is truncated on write (the format you typed has nowhere to put it), so round-tripping a finer-than-seconds value through a%Spattern is lossy by design. Use the default ISO 8601 representation when you need sub-second fidelity. Explicit-width fraction tokens (%3S/%6S/%9S) are a planned extension.- Text/JSON-family backends only.
date_formatis a textual wrapper that routes through the JSON serializer, so it also works for the JSON-family text outputs (NDJSON, stencil/mustache). Serializing a field that uses it to a backend with its own native encoding (BEVE, CBOR, MsgPack, BSON, or TOML) is a compile error (an undefinedglz::to<Format, …>) rather than a silent miscoding.
glz::epoch_count — per-field Unix timestamp¶
glz::epoch_count<Duration>(&T::member) serializes a system_clock time point as a numeric Unix timestamp in units of Duration. It is the per-field counterpart to the glz::epoch_time storage wrapper, letting one field be an epoch count while others stay ISO 8601:
#include "glaze/chrono.hpp"
struct Reading {
std::chrono::system_clock::time_point observed{}; // ISO 8601 (default)
std::chrono::sys_time<std::chrono::milliseconds> logged{};
};
template <>
struct glz::meta<Reading> {
using T = Reading;
static constexpr auto value = glz::object(
"observed", &T::observed, // "2026-06-18T12:34:56Z"
"logged", glz::epoch_count<std::chrono::milliseconds>(&T::logged)); // 1781786096789
};
Unlike glz::epoch_time<Duration> (a storage type you declare your member as), glz::epoch_count wraps an ordinary system_clock time-point member in place, so you can keep the field's native type. Like date_format, it routes through the JSON serializer (so it also works for NDJSON and stencil output) and is a compile error under the native-encoding backends (BEVE, CBOR, MsgPack, BSON, TOML).
The count is read as a JSON integer: exponent form (1e3 → 1000) is accepted, while fractional values (1.5) and quoted numbers ("1500") are rejected with error_code::parse_error.
Complete Example¶
#include "glaze/glaze.hpp"
#include <chrono>
struct LogEntry {
std::string message;
std::chrono::system_clock::time_point timestamp;
std::chrono::microseconds processing_time;
};
struct MetricsReport {
glz::epoch_millis generated_at;
std::chrono::steady_clock::time_point uptime_start;
std::vector<LogEntry> entries;
};
int main() {
MetricsReport report;
report.generated_at = std::chrono::system_clock::now();
report.uptime_start = std::chrono::steady_clock::now();
report.entries.push_back({
"Request processed",
std::chrono::system_clock::now(),
std::chrono::microseconds{1234}
});
// Serialize
std::string json = glz::write_json(report).value();
// Deserialize
MetricsReport parsed;
glz::read_json(parsed, json);
}
Output:
{
"generated_at": 1702481400123,
"uptime_start": 123456789012345,
"entries": [
{
"message": "Request processed",
"timestamp": "2024-12-13T15:30:45.123456Z",
"processing_time": 1234
}
]
}
Summary Table¶
| Type | JSON Format | Example |
|---|---|---|
std::chrono::duration<Rep, Period> |
Numeric count | 12345 |
std::chrono::system_clock::time_point |
ISO 8601 string | "2024-12-13T15:30:45Z" |
std::chrono::sys_days (sys_time<days>) |
Date string | "2024-12-13" |
std::chrono::year_month_day |
Date string | "2024-12-13" |
std::chrono::steady_clock::time_point |
Numeric count | 123456789012345 |
glz::epoch_seconds |
Unix seconds | 1702481400 |
glz::epoch_millis |
Unix milliseconds | 1702481400123 |
glz::epoch_micros |
Unix microseconds | 1702481400123456 |
glz::epoch_nanos |
Unix nanoseconds | 1702481400123456789 |
Notes¶
- Thread Safety: All chrono serialization is fully thread-safe with no static buffers
- Precision: Roundtrip serialization preserves full precision for all supported types
- Validation: Invalid ISO 8601 strings return
glz::error_code::parse_error - Timezone Handling: Time points are always converted to/from UTC; timezone offsets in input are properly applied
- Leap Seconds: Not supported (
23:59:60will return a parse error).std::chrono::system_clockuses Unix time which does not account for leap seconds
TOML Datetime Support¶
TOML has native datetime types (not quoted strings). When using glz::write_toml / glz::read_toml, chrono types use TOML's native datetime format:
system_clock::time_point→ TOML Offset Date-Time (2024-06-15T10:30:45Z)year_month_day→ TOML Local Date (2024-06-15)hh_mm_ss<Duration>→ TOML Local Time (10:30:45.123)- Durations and other time points → Numeric values
The per-field
glz::date_format/glz::epoch_countwrappers are JSON-text-only and do not compile underglz::write_toml. Native (unwrapped) chrono members serialize as TOML datetimes as shown above; only the per-field format wrappers are restricted.
See TOML Documentation for full details.
CBOR Datetime Support¶
CBOR has standard semantic tags for dates and times defined in RFC 8949. When using glz::write_cbor / glz::read_cbor, chrono types use these tags:
system_clock::time_point→ tag 0 + RFC 3339 date/time text string (2024-06-15T10:30:45Z)epoch_seconds→ tag 1 + integer seconds since Unix epochepoch_millis/epoch_micros/epoch_nanos→ tag 1 + float64 seconds since epochstd::chrono::duration→ bare integer count in the duration's native unitsstd::chrono::steady_clock::time_point→ bare integer count (no tag)
Sub-second epoch_time precision is carried via float64 because RFC 8949 §3.4.2 forbids nested tags (including tag 4 decimal fractions) as tag 1 content. epoch_millis and epoch_micros round-trip losslessly for realistic epochs; epoch_nanos loses ~100 ns of precision on modern timestamps once the nanosecond count exceeds 2^53.
See CBOR Documentation for wire-format tables, decoder leniency rules, and safety guarantees.