Skip to content

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_days previously 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_day field 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 with error_code::constraint_violated rather than emitting wrap-around digits.
  • %S writes 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 %S pattern 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_format is 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 undefined glz::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 (1e31000) 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:60 will return a parse error). std::chrono::system_clock uses 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_count wrappers are JSON-text-only and do not compile under glz::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 epoch
  • epoch_millis / epoch_micros / epoch_nanos → tag 1 + float64 seconds since epoch
  • std::chrono::duration → bare integer count in the duration's native units
  • std::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.