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 (the rep, expressed in the duration's own period). This representation is shared by every Glaze format (JSON, BEVE, CBOR, MsgPack, BSON, TOML, YAML, CSV, JSONB, EETF), so the same type round-trips between any of them interchangeably:
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
// The same value over a binary format: BEVE encodes the duration byte-for-byte
// like its underlying integer rep.
std::string beve{};
glz::write_beve(ms, beve);
std::chrono::milliseconds from_beve{};
glz::read_beve(from_beve, beve); // from_beve == ms
In BSON, whose document root cannot be a bare scalar, a duration is supported as a struct/map field (encoded as the rep's native BSON number type) rather than as a top-level value.
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
High Resolution Clock Time Points¶
std::chrono::high_resolution_clock is not a distinct clock in any mainstream standard library. It is an alias, and which clock it aliases differs by implementation:
| Standard library | high_resolution_clock is |
high_resolution_clock::time_point behaves as |
|---|---|---|
| libc++ (Clang), MSVC STL | steady_clock |
numeric count, supported by every format |
| libstdc++ (GCC) | system_clock |
ISO 8601 string, only in the formats that support system_clock::time_point |
Because the alias makes the two types identical, Glaze cannot give them different behavior. The practical consequence is that a type containing an hrc::time_point can compile on one platform and fail on another: under libstdc++ it is a system_clock::time_point, which BEVE, CSV, and JSONB do not support (see Format Support).
Prefer naming the clock you actually mean:
std::chrono::steady_clock::time_point t; // portable: numeric count in every format
std::chrono::system_clock::time_point w; // portable: wall clock, format support varies
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 |
Format Support¶
Durations and count-based time points use one representation shared by every format, so they interchange freely: the same meta schema can target JSON and BEVE. Calendar types are format-specific, because a wall-clock instant has no single natural encoding across text and binary formats.
| Type | JSON | BEVE | CBOR | MsgPack | BSON | TOML | YAML | CSV | JSONB |
|---|---|---|---|---|---|---|---|---|---|
std::chrono::duration<Rep, Period> |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
steady_clock::time_point |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
system_clock::time_point |
✅ | — | ✅ | ✅ | ✅ | ✅ | ✅ | — | — |
sys_days (sys_time<days>) |
✅ | — | ✅ | — | — | ✅ | ✅ | — | — |
year_month_day |
✅ | — | — | — | — | ✅ | ✅ | — | — |
glz::epoch_time<D> |
✅ | — | ✅ | — | — | — | — | — | — |
high_resolution_clock::time_point is not listed because it is an alias: it follows the steady_clock row under libc++ and MSVC, and the system_clock row under libstdc++. See High Resolution Clock Time Points.
An unsupported combination is a compile-time error (write_supported / read_supported report false), never a silent fallback. To carry a wall-clock instant through a format that lacks calendar support, store it as a duration since a known epoch (for example a std::chrono::milliseconds field), which every format encodes identically.
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.