Skip to content

# TOML (Tom's Obvious, Minimal Language)

Glaze ships with a fast TOML 1.0 reader and writer. The same compile-time reflection metadata you already use for JSON works for TOML, so you can reuse your `glz::meta` specializations without additional boilerplate.

## Getting Started

The header `glaze/toml.hpp` exposes the high-level helpers. The example below writes and reads a configuration struct:

    #include "glaze/toml.hpp"

    struct retry_policy
    {
       int attempts = 5;
       int backoff_ms = 250;
    };

    template <>
    struct glz::meta<retry_policy>
    {
       using T = retry_policy;
       static constexpr auto value = object(&T::attempts, &T::backoff_ms);
    };

    struct app_config
    {
       std::string host = "127.0.0.1";
       int port = 8080;
       retry_policy retry{};
       std::vector<std::string> features{"metrics"};
    };

    template <>
    struct glz::meta<app_config>
    {
       using T = app_config;
       static constexpr auto value = object(&T::host, &T::port, &T::retry, &T::features);
    };

    app_config cfg{};
    std::string toml{};
    auto write_error = glz::write_toml(cfg, toml);
    if (write_error) {
       const auto message = glz::format_error(write_error, toml);
       // handle the error message
    }

    app_config loaded{};
    auto read_error = glz::read_toml(loaded, toml);
    if (read_error) {
       const auto message = glz::format_error(read_error, toml);
       // handle the error message
    }

`glz::write_toml` and `glz::read_toml` return an `error_ctx`. The object becomes truthy when an error occurred; pass it to `glz::format_error` to obtain a human-readable explanation.

## TOML Input Example

The `app_config` structure above accepts both inline tables and dotted keys. Either of the snippets below will populate the same object:

    host = "0.0.0.0"
    port = 9000
    features = ["metrics", "debug"]

    retry = { attempts = 6, backoff_ms = 500 }

    host = "0.0.0.0"
    port = 9000
    features = ["metrics", "debug"]

    retry.attempts = 6
    retry.backoff_ms = 500

Glaze understands standard TOML number formats (binary, octal, hex), quoted and multiline strings, arrays, inline tables, and comments (`#`).

## Array of Tables

Glaze supports TOML's array-of-tables syntax (`[[array_name]]`) for serializing and deserializing `std::vector` of objects. This provides a clean, readable format for arrays of structured data.

### Basic Array of Tables

    struct product
    {
       std::string name;
       int sku;
    };

    template <>
    struct glz::meta<product>
    {
       using T = product;
       static constexpr auto value = object(&T::name, &T::sku);
    };

    struct catalog
    {
       std::string store_name;
       std::vector<product> products;
    };

    template <>
    struct glz::meta<catalog>
    {
       using T = catalog;
       static constexpr auto value = object(&T::store_name, &T::products);
    };

    catalog c{
       "Hardware Store",
       {{"Hammer", 738594937}, {"Nail", 284758393}}
    };

    std::string toml{};
    glz::write_toml(c, toml);

Output:

    store_name = "Hardware Store"
    [[products]]
    name = "Hammer"
    sku = 738594937

    [[products]]
    name = "Nail"
    sku = 284758393

### Nested Array of Tables

Glaze produces TOML-spec-compliant output for nested arrays using dotted paths (`[[parent.child]]`):

    struct variety
    {
       std::string name;
    };

    struct fruit
    {
       std::string name;
       std::vector<variety> varieties;
    };

    struct fruit_basket
    {
       std::vector<fruit> fruits;
    };

    fruit_basket basket{
       {{"apple", {{"red delicious"}, {"granny smith"}}},
        {"banana", {{"cavendish"}}}}
    };

    std::string toml{};
    glz::write_toml(basket, toml);

Output:

    [[fruits]]
    name = "apple"
    [[fruits.varieties]]
    name = "red delicious"

    [[fruits.varieties]]
    name = "granny smith"

    [[fruits]]
    name = "banana"
    [[fruits.varieties]]
    name = "cavendish"

### Reading Array of Tables

Glaze reads array-of-tables syntax correctly, including: - Multiple `[[name]]` sections that append to the same array - Empty table entries (`[[name]]` followed immediately by another `[[name]]`) - Nested dotted paths like `[[parent.child]]`

    std::string input = R"(
    [[products]]
    name = "Hammer"
    sku = 738594937

    [[products]]

    [[products]]
    name = "Nail"
    sku = 284758393
    )";

    catalog c{};
    glz::read_toml(c, input);
    // c.products.size() == 3 (second entry is empty/default)

### Write Ordering

Glaze writes TOML in spec-compliant order: scalar key-value pairs appear before tables and array-of-tables sections. This ensures the output is valid TOML that can be parsed by any compliant reader.

### Inline Tables

By default, `std::vector` of objects uses array-of-tables (`[[name]]`) syntax. There are two ways to use inline table syntax (`[{...}, {...}]`) instead:

#### Global Option: `glz::toml_opts`

Use `glz::toml_opts` with the standard `write<>` interface to write all arrays of objects using inline syntax:

    struct product
    {
       std::string name;
       int sku;
    };

    struct catalog
    {
       std::string store_name;
       std::vector<product> products;
    };

    catalog c{"My Store", {{"Widget", 100}, {"Gadget", 200}}};
    std::string toml{};
    glz::write<glz::toml_opts{true}>(c, toml);  // inline_arrays = true

Output:

    store_name = "My Store"
    products = [{name = "Widget", sku = 100}, {name = "Gadget", sku = 200}]

The `glz::toml_opts` struct inherits from `glz::opts`, following the recommended pattern for format-specific options. For repeated use, create a named constant:

    constexpr glz::toml_opts inline_toml{true};
    glz::write<inline_toml>(c, toml);

#### Per-Field Option: `glz::inline_table` Wrapper

For fine-grained control, use the `glz::inline_table` wrapper in your `glz::meta` definition to specify which fields use inline syntax:

    template <>
    struct glz::meta<catalog>
    {
       using T = catalog;
       // Use inline_table wrapper for this specific field
       static constexpr auto value = object(&T::store_name, "products", glz::inline_table<&T::products>);
    };

    catalog c{"My Store", {{"Widget", 100}, {"Gadget", 200}}};
    std::string toml{};
    glz::write_toml(c, toml);  // Regular write_toml, but products uses inline syntax

This is useful when you want a more compact representation or when the array contains simple objects with few fields.

## Using the Generic API

The convenience wrappers call into the generic `glz::read`/`glz::write` pipeline. You can reuse the same options struct you already use for JSON while switching the format to TOML:

    std::string_view config_text = R"(
    host = "0.0.0.0"
    port = 9000
    retry.attempts = 4
    retry.backoff_ms = 200
    extra.flag = true
    )";

    app_config cfg{};
    auto ec = glz::read<glz::opts{.format = glz::TOML, .error_on_unknown_keys = false}>(cfg, config_text);
    if (ec) {
       const auto message = glz::format_error(ec, config_text);
       // handle unknown field or parse problems
    }

Setting `.error_on_unknown_keys = false` allows dotted keys that do not correspond to reflected members to be skipped gracefully. Any other option in `glz::opts` (for example `.skip_null_members` or `.error_on_missing_keys`) can be combined the same way.

The write side uses the same mechanism:

    std::string toml{};
    auto write_ec = glz::write<glz::opts{.format = glz::TOML, .skip_null_members = false}>(cfg, toml);
    if (write_ec) {
       const auto message = glz::format_error(write_ec, toml);
       // handle write problems
    }

Both `glz::read` and `glz::write` return `error_ctx`, so remember to check the result in production code.

## File Helpers and Buffers

For convenience Glaze also provides file-oriented helpers:

    std::string buffer{};
    glz::write_file_toml(cfg, "config.toml", buffer); // writes to disk when serialization succeeds

    app_config loaded{};
    glz::read_file_toml(loaded, "config.toml", buffer);

`glz::read_toml` works with `std::string`, `std::string_view`, or any contiguous character buffer.

## Datetime Support

Glaze fully supports TOML v1.1.0 datetime types, which are first-class values in TOML (not quoted strings). This enables seamless serialization of `std::chrono` types with native TOML datetime format.

### TOML Datetime Types

TOML defines four datetime types, each mapping to specific C++ chrono types:

TOML Type | C++ Type | Format Example
--- | --- | ---
Offset Date-Time | `std::chrono::system_clock::time_point` | `2024-06-15T10:30:45Z`
Local Date-Time | `std::chrono::system_clock::time_point` | `2024-06-15T10:30:45`
Local Date | `std::chrono::year_month_day` | `2024-06-15`
Local Time | `std::chrono::hh_mm_ss<Duration>` | `10:30:45.123`

### Offset Date-Time (system_clock::time_point)

`std::chrono::system_clock::time_point` serializes as an unquoted TOML Offset Date-Time in UTC:

    #include "glaze/toml.hpp"
    #include <chrono>

    auto now = std::chrono::system_clock::now();
    std::string toml = glz::write_toml(now).value();
    // Output: 2024-12-13T15:30:45Z (unquoted)

The parser supports multiple RFC 3339 formats:

    std::chrono::system_clock::time_point tp;

    // UTC with Z suffix
    glz::read_toml(tp, "2024-12-13T15:30:45Z");

    // Lowercase z is allowed
    glz::read_toml(tp, "2024-12-13T15:30:45z");

    // Space delimiter instead of T (per TOML spec)
    glz::read_toml(tp, "2024-12-13 15:30:45Z");

    // With timezone offset
    glz::read_toml(tp, "2024-12-13T15:30:45+05:00");
    glz::read_toml(tp, "2024-12-13T15:30:45-08:00");

    // With fractional seconds
    glz::read_toml(tp, "2024-12-13T15:30:45.123456Z");

    // Without seconds (per TOML spec)
    glz::read_toml(tp, "2024-12-13T15:30Z");

    // Local Date-Time (no timezone - treated as UTC)
    glz::read_toml(tp, "2024-12-13T15:30:45");

### Local Date (year_month_day)

`std::chrono::year_month_day` serializes as an unquoted TOML Local Date:

    using namespace std::chrono;

    year_month_day date{year{2024}, month{6}, day{15}};
    std::string toml = glz::write_toml(date).value();
    // Output: 2024-06-15 (unquoted)

    // Reading
    year_month_day parsed;
    glz::read_toml(parsed, "2024-12-25");
    // parsed.year() == 2024, parsed.month() == December, parsed.day() == 25

### Local Time (hh_mm_ss)

`std::chrono::hh_mm_ss<Duration>` serializes as an unquoted TOML Local Time:

    using namespace std::chrono;

    // Seconds precision
    hh_mm_ss<seconds> time_sec{hours{10} + minutes{30} + seconds{45}};
    std::string toml = glz::write_toml(time_sec).value();
    // Output: 10:30:45

    // Milliseconds precision
    hh_mm_ss<milliseconds> time_ms{hours{10} + minutes{30} + seconds{45} + milliseconds{123}};
    toml = glz::write_toml(time_ms).value();
    // Output: 10:30:45.123

Reading supports fractional seconds and optional seconds:

    using namespace std::chrono;

    hh_mm_ss<milliseconds> time{milliseconds{0}};

    // Standard format
    glz::read_toml(time, "23:59:59");

    // With fractional seconds
    glz::read_toml(time, "12:30:45.500");

    // Without seconds (per TOML spec)
    glz::read_toml(time, "14:30");

### Structs with Datetime Fields

Datetime types work seamlessly in structs:

    struct Event {
        std::string name;
        std::chrono::system_clock::time_point timestamp;
        std::chrono::year_month_day date;
        std::chrono::hh_mm_ss<std::chrono::seconds> start_time;
    };

    Event event{
        "Meeting",
        std::chrono::system_clock::now(),
        std::chrono::year_month_day{std::chrono::year{2024}, std::chrono::month{6}, std::chrono::day{15}},
        std::chrono::hh_mm_ss<std::chrono::seconds>{std::chrono::hours{14} + std::chrono::minutes{30}}
    };

    auto toml = glz::write_toml(event).value();

Output:

    name = "Meeting"
    timestamp = 2024-06-15T14:30:00Z
    date = 2024-06-15
    start_time = 14:30:00

### Duration Types

`std::chrono::duration` types serialize as their numeric count value (not as TOML datetime):

    std::chrono::seconds sec{3600};
    std::string toml = glz::write_toml(sec).value();  // "3600"

    std::chrono::milliseconds ms{};
    glz::read_toml(ms, "12345");  // ms.count() == 12345

This works with any duration type including custom periods:

    std::chrono::hours h{24};               // "24"
    std::chrono::nanoseconds ns{123456789}; // "123456789"

    // Floating-point rep
    std::chrono::duration<double, std::milli> ms{123.456};  // "123.456"

### Steady Clock and High Resolution Clock

`std::chrono::steady_clock::time_point` and `std::chrono::high_resolution_clock::time_point` serialize as numeric counts, since their epochs are implementation-defined:

    auto start = std::chrono::steady_clock::now();
    std::string toml = glz::write_toml(start).value();  // numeric count

    std::chrono::steady_clock::time_point parsed;
    glz::read_toml(parsed, toml);  // exact roundtrip

### Datetime Summary Table

C++ Type | TOML Format | Example Output
--- | --- | ---
`system_clock::time_point` | Offset Date-Time | `2024-06-15T10:30:45Z`
`year_month_day` | Local Date | `2024-06-15`
`hh_mm_ss<seconds>` | Local Time | `10:30:45`
`hh_mm_ss<milliseconds>` | Local Time | `10:30:45.123`
`duration<Rep, Period>` | Numeric | `3600`
`steady_clock::time_point` | Numeric | `123456789012345`

## Variant and Generic Type Support

Glaze supports `std::variant` and the generic JSON types (`glz::generic`, `glz::generic_i64`, `glz::generic_u64`) for TOML serialization and deserialization. This enables schema-less parsing where the structure of the data is not known at compile time.

### std::variant Support

Any `std::variant` can be serialized to TOML. When writing, the currently held alternative is serialized directly:

    #include "glaze/toml.hpp"

    std::variant<int, double, std::string, bool> value = 42;
    std::string toml = glz::write_toml(value).value();
    // Output: 42

    value = "hello";
    toml = glz::write_toml(value).value();
    // Output: "hello"

    value = true;
    toml = glz::write_toml(value).value();
    // Output: true

When reading, Glaze automatically detects the TOML value type and selects the appropriate variant alternative:

    std::variant<int64_t, double, std::string, bool> value;

    glz::read_toml(value, "42");        // value holds int64_t{42}
    glz::read_toml(value, "3.14");      // value holds double{3.14}
    glz::read_toml(value, "\"text\"");  // value holds std::string{"text"}
    glz::read_toml(value, "true");      // value holds bool{true}

### Generic JSON Types

The generic JSON types provide a convenient way to parse arbitrary TOML data:

Type | Integer Storage | Use Case
--- | --- | ---
`glz::generic` | `double` | General purpose, preserves floating-point precision
`glz::generic_i64` | `int64_t` | When integers must be preserved exactly
`glz::generic_u64` | `uint64_t` for positive, `int64_t` for negative | When large positive integers are needed

#### Using glz::generic

    #include "glaze/toml.hpp"

    glz::generic data;
    std::string input = R"(
    name = "config"
    port = 8080
    rate = 0.5
    enabled = true
    tags = ["web", "api"]
    )";

    auto ec = glz::read_toml(data, input);

    // Access the parsed data
    auto& obj = std::get<glz::obj>(data);
    auto& name = std::get<std::string>(obj["name"]);   // "config"
    auto& port = std::get<double>(obj["port"]);        // 8080.0
    auto& rate = std::get<double>(obj["rate"]);        // 0.5
    auto& enabled = std::get<bool>(obj["enabled"]);    // true
    auto& tags = std::get<glz::arr>(obj["tags"]);      // ["web", "api"]

#### Using glz::generic_i64

Use `glz::generic_i64` when you need exact integer preservation:

    glz::generic_i64 data;
    glz::read_toml(data, "value = 9007199254740993");  // Larger than JS safe integer

    auto& obj = std::get<glz::obj_i64>(data);
    auto& value = std::get<int64_t>(obj["value"]);  // Exact: 9007199254740993

#### Using glz::generic_u64

Use `glz::generic_u64` when working with large unsigned integers:

    glz::generic_u64 data;
    glz::read_toml(data, "big = 18446744073709551615");  // Max uint64_t

    auto& obj = std::get<glz::obj_u64>(data);
    auto& big = std::get<uint64_t>(obj["big"]);  // 18446744073709551615

Negative integers are stored as `int64_t` even in u64 mode:

    glz::generic_u64 data;
    glz::read_toml(data, "negative = -42");

    auto& obj = std::get<glz::obj_u64>(data);
    auto& negative = std::get<int64_t>(obj["negative"]);  // -42 as int64_t

### Type Detection Rules

When reading into a variant or generic type, Glaze uses these rules to determine the TOML value type:

TOML Syntax | Detected Type
--- | ---
`"..."` or `'...'` | String
`true` or `false` | Boolean
`[...]` | Array
Numbers with `.`, `e`, `E`, `inf`, `nan` | Float
Other numbers | Integer

For integers in `glz::generic_u64` mode: - Numbers starting with `-` are stored as `int64_t` - Positive numbers are stored as `uint64_t`

### Writing Generic Types

Generic types can be written back to TOML:

    glz::generic_i64 data;
    auto& obj = data.emplace<glz::obj_i64>();
    obj["name"] = "example";
    obj["count"] = int64_t{42};
    obj["enabled"] = true;

    std::string toml = glz::write_toml(data).value();

Output:

    name = "example"
    count = 42
    enabled = true

### Nested Arrays

Nested arrays are supported for reading:

    glz::generic data;
    glz::read_toml(data, "[[1, 2], [3, 4], [5, 6]]");

    auto& arr = std::get<glz::arr>(data);
    auto& inner = std::get<glz::arr>(arr[0]);
    auto& val = std::get<double>(inner[0]);  // 1.0

### Map Types (std::map, std::unordered_map)

TOML documents can also be read directly into map types like `std::map<std::string, T>` or `std::unordered_map<std::string, T>`:

    std::map<std::string, int64_t> config;
    std::string toml = R"(
    port = 8080
    timeout = 30
    retries = 3
    )";

    auto ec = glz::read_toml(config, toml);
    // config["port"] == 8080
    // config["timeout"] == 30
    // config["retries"] == 3

This also works with table sections:

    std::map<std::string, std::map<std::string, std::string>> config;
    std::string toml = R"(
    [database]
    host = "localhost"
    user = "admin"

    [cache]
    driver = "redis"
    )";

    auto ec = glz::read_toml(config, toml);
    // config["database"]["host"] == "localhost"
    // config["cache"]["driver"] == "redis"

### Limitations

- Null values: TOML has no native null type. When writing `std::nullptr_t` or a variant holding null, an empty string `""` is written.
- Type coercion: The parser does not coerce types. If the variant has no matching alternative for the detected type, an error is returned.
- Array of tables in maps: The `[[array_of_tables]]` syntax is not fully supported when reading into map types. Use struct-based types for this pattern.