{"id":50312448,"url":"https://github.com/aurimasniekis/cpp-metadata","last_synced_at":"2026-05-28T22:01:35.917Z","repository":{"id":359997758,"uuid":"1248296245","full_name":"aurimasniekis/cpp-metadata","owner":"aurimasniekis","description":"Modern C++23 header-only metadata container (JSON-like)","archived":false,"fork":false,"pushed_at":"2026-05-24T13:33:36.000Z","size":56,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-24T15:24:09.190Z","etag":null,"topics":["cpp","cpp23","header-only","header-only-library","json","metadata"],"latest_commit_sha":null,"homepage":"https://aurimasniekis.github.io/cpp-metadata/","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aurimasniekis.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-24T13:05:50.000Z","updated_at":"2026-05-24T13:33:39.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/aurimasniekis/cpp-metadata","commit_stats":null,"previous_names":["aurimasniekis/cpp-metadata"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/aurimasniekis/cpp-metadata","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurimasniekis%2Fcpp-metadata","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurimasniekis%2Fcpp-metadata/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurimasniekis%2Fcpp-metadata/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurimasniekis%2Fcpp-metadata/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aurimasniekis","download_url":"https://codeload.github.com/aurimasniekis/cpp-metadata/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurimasniekis%2Fcpp-metadata/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33627941,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-28T02:00:06.440Z","response_time":99,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cpp","cpp23","header-only","header-only-library","json","metadata"],"created_at":"2026-05-28T22:01:35.136Z","updated_at":"2026-05-28T22:01:35.910Z","avatar_url":"https://github.com/aurimasniekis.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Metadata\n\n[![CI](https://github.com/aurimasniekis/cpp-metadata/actions/workflows/ci.yml/badge.svg)](https://github.com/aurimasniekis/cpp-metadata/actions/workflows/ci.yml)\n[![Docs](https://github.com/aurimasniekis/cpp-metadata/actions/workflows/docs.yml/badge.svg)](https://aurimasniekis.github.io/cpp-metadata/)\n\nA small, header-only C++23 library for building **typed dynamic key/value\ntrees** that look and behave like a JSON document in memory. Use it whenever\nyou would otherwise reach for `std::unordered_map\u003cstd::string, std::any\u003e` to\nhold a bag of mixed-type properties: device/sensor metadata, configuration\ntrees, plugin parameters, scratch attributes on records, etc.\n\nThe CMake project and target are named `metadata`, but the C++ namespace is\n`md`. There is no parser, no schema, and no networking layer — just the\nin-memory shape, with an optional nlohmann/json bridge for the rare moment\nyou actually want to round-trip to text.\n\n---\n\n## Why use this library?\n\n- **Good for** small-to-mid in-memory property bags that mix booleans,\n  integers, floats, strings, arrays, and nested objects.\n- **Good for** \"JSON-shaped data without a JSON dependency\": the optional\n  `\u003cmd/json.hpp\u003e` adapter is opt-in, and the core compiles with only the\n  C++23 standard library.\n- **Good for** path-style access: `m.require_path(\"device.channels[0].freq_hz\")`.\n- **Avoids** the classic `const char*` → `bool` trap that variant-based\n  JSON value types suffer from. `Value{\"x\"}` is always a string;\n  arbitrary pointer types are explicitly `= delete`'d.\n- **Avoids** silent conflation of integer kinds. `int`, `unsigned`, `float`,\n  and `double` are distinct alternatives in `Value` and stay that way (with\n  one documented exception around the JSON adapter — see below).\n- **Not ideal for** workloads that need insertion-order preservation;\n  `Object` is backed by `std::unordered_map`.\n- **Not ideal for** full JSONPath / RFC 6901 / JSON Pointer; the path\n  syntax is dot + bracket only.\n- **Not ideal for** parsing JSON text. The library never parses strings;\n  if you need parsing, use nlohmann/json (or any other parser) and convert\n  via the bundled adapter.\n\n---\n\n## Quick example\n\n```cpp\n#include \u003cmd/metadata.hpp\u003e\n\n#include \u003ciostream\u003e\n\nint main() {\n    md::Metadata m;                    // alias for md::Object\n    m[\"name\"]    = \"sensor-7\";         // string\n    m[\"enabled\"] = true;               // bool\n    m[\"count\"]   = 42;                 // signed int -\u003e int64\n    m[\"weight\"]  = 3.14;               // double\n    m[\"tags\"]    = {\"alpha\", \"beta\"};  // Array (bare braced elements)\n\n    // Nested object: a braced list of {key, value} pairs.\n    m[\"device\"] = {\n        {\"id\",       \"abc-123\"},\n        {\"firmware\", {{\"major\", 1}, {\"minor\", 4}}},\n    };\n\n    // Compact JSON-like output via operator\u003c\u003c.\n    std::cout \u003c\u003c m \u003c\u003c '\\n';\n\n    // Typed retrieval — throws md::missing_key_error / md::type_error on misuse.\n    std::cout \u003c\u003c m.require_string(\"name\") \u003c\u003c '\\n';\n    std::cout \u003c\u003c m.require_path(\"device.firmware.major\").as_int() \u003c\u003c '\\n';\n    std::cout \u003c\u003c m.require_path(\"tags[0]\").as_string() \u003c\u003c '\\n';\n}\n```\n\nWhat is going on:\n\n- `md::Metadata` is a type alias for `md::Object`. It has the surface of\n  `std::unordered_map\u003cstd::string, md::Value\u003e` plus the metadata helpers\n  (`require_*`, `find_path`, `merge`, …).\n- The braced list `{\"alpha\", \"beta\"}` resolves to an `Array` because every\n  element is a single `Value`. The list of `{key, value}` pairs resolves to\n  an `Object` because no `Value` constructor takes two arguments — only\n  the pair-shaped `operator=` matches. That disambiguation is deliberate.\n- `operator\u003c\u003c` produces a compact JSON-shaped string. Floating-point values\n  use `std::to_chars` shortest round-trip, so `3.14` prints as `3.14`, not\n  `3.1400000000000001`.\n\n---\n\n## Installation\n\nThe library is a header-only CMake `INTERFACE` target. The minimum CMake\nversion is 3.25.\n\n### CMake — FetchContent\n\n```cmake\ncmake_minimum_required(VERSION 3.25)\nproject(my_app LANGUAGES CXX)\n\nset(CMAKE_CXX_STANDARD 23)\nset(CMAKE_CXX_STANDARD_REQUIRED ON)\n\ninclude(FetchContent)\nFetchContent_Declare(\n    metadata\n    URL      https://github.com/aurimasniekis/cpp-metadata/archive/refs/tags/v0.2.0.tar.gz\n    URL_HASH SHA256=dabd8a872a67c96a45d07a50a4d69abc273b05ba318628cd46f76ef909561db1\n    DOWNLOAD_EXTRACT_TIMESTAMP TRUE\n)\nFetchContent_MakeAvailable(metadata)\n\nadd_executable(my_app main.cpp)\ntarget_link_libraries(my_app PRIVATE metadata::metadata)\n```\n\n`nlohmann/json` (3.12.0) is fetched the same way when JSON support is\nenabled, but a `find_package`-installed copy is preferred (see\n`cmake/Dependencies.cmake`).\n\n### CMake — add_subdirectory\n\nIf you vendor the source into a subdirectory:\n\n```cmake\nadd_subdirectory(third_party/metadata)\ntarget_link_libraries(my_app PRIVATE metadata::metadata)\n```\n\n### CMake — `find_package` after install\n\nIf the library was installed with `cmake --install \u003cbuild\u003e`:\n\n```cmake\nfind_package(metadata 0.2 REQUIRED)\ntarget_link_libraries(my_app PRIVATE metadata::metadata)\n```\n\nInstall rules are auto-disabled when `nlohmann_json` was fetched (a fetched\ndependency cannot be re-exported by an installed package). To install,\neither provide `nlohmann_json` via the system package manager / a prior\n`find_package` install, or disable JSON support with\n`-DMETADATA_WITH_NLOHMANN_JSON=OFF`.\n\n### Manual drop-in\n\nCopy the `include/md/` directory into your include path. The core requires\nonly the C++23 standard library. `\u003cmd/json.hpp\u003e` `#error`s out at the top\nof the file if `\u003cnlohmann/json.hpp\u003e` is missing — the failure is loud and\nimmediate, not a silent miss.\n\n\u003e The `\u003cmd/version.hpp\u003e` header is generated by CMake from\n\u003e `version.hpp.in`. If you copy headers manually, you have to provide your\n\u003e own `version.hpp` or skip including it.\n\n---\n\n## Requirements\n\n- **C++ standard**: C++23 — the implementation uses `\u003cformat\u003e`, concepts,\n  `std::variant`, `std::to_chars` for `double`, and heterogeneous lookup\n  with a transparent hash.\n- **CMake**: ≥ 3.25 when building via CMake.\n- **Optional dependency**: [`nlohmann/json`](https://github.com/nlohmann/json)\n  ≥ 3.12, only when `METADATA_WITH_NLOHMANN_JSON=ON` (the default).\n\n---\n\n## Core concepts\n\nThe public surface is small. The headers that matter day-to-day are:\n\n| Header              | Provides                                        |\n|---------------------|-------------------------------------------------|\n| `\u003cmd/metadata.hpp\u003e` | Umbrella header. Pulls in everything below.     |\n| `\u003cmd/value.hpp\u003e`    | `Value`, `Array`, the constrained constructors. |\n| `\u003cmd/object.hpp\u003e`   | `Object` (aliased as `Metadata`).               |\n| `\u003cmd/path.hpp\u003e`     | `find_path`, `require_path`, `contains_path`.   |\n| `\u003cmd/helpers.hpp\u003e`  | Free-function forwarders (`md::contains`, …).   |\n| `\u003cmd/ostream.hpp\u003e`  | `operator\u003c\u003c` for `Value`, `Object`, `Array`.    |\n| `\u003cmd/format.hpp\u003e`   | `std::formatter` specializations.               |\n| `\u003cmd/hash.hpp\u003e`     | `std::hash\u003cValue\u003e` / `\u003cObject\u003e` / `\u003cArray\u003e`.    |\n| `\u003cmd/json.hpp\u003e`     | Optional nlohmann/json adapter.                 |\n| `\u003cmd/parcel.hpp\u003e`   | Optional cpp-parcel adapter (cell wrappers).    |\n| `\u003cmd/error.hpp\u003e`    | `md::error`, `missing_key_error`, `type_error`. |\n\nJust `#include \u003cmd/metadata.hpp\u003e` and you get the full API. The JSON\nadapter is auto-included via `__has_include(\u003cnlohmann/json.hpp\u003e)`, and\nthe parcel adapter via `__has_include(\u003cparcel/parcel.h\u003e)`.\n\n### `md::Value`\n\n`Value` is a discriminated union with these alternatives:\n\n| Alternative      | Predicate     | Strict accessor  | Pointer accessor (noexcept) |\n|------------------|---------------|------------------|-----------------------------|\n| `std::nullptr_t` | `is_null()`   | —                | —                           |\n| `bool`           | `is_bool()`   | `as_bool()`      | `as_bool_if()`              |\n| `std::int64_t`   | `is_int()`    | `as_int()`       | `as_int_if()`               |\n| `std::uint64_t`  | `is_uint()`   | `as_uint()`      | `as_uint_if()`              |\n| `float`          | `is_float()`  | `as_float()`     | `as_float_if()`             |\n| `double`         | `is_double()` | `as_double()` \\* | `as_double_if()`            |\n| `std::string`    | `is_string()` | `as_string()`    | `as_string_if()`            |\n| `Array`          | `is_array()`  | `as_array()`     | `as_array_if()`             |\n| `Object`         | `is_object()` | `as_object()`    | `as_object_if()`            |\n\n`is_number()` is true if any of `int`, `uint`, `float`, or `double` holds.\n\n\\* **`as_double()` is the only widening accessor.** It accepts `int64`,\n`uint64`, `float`, or `double` and returns a `double`. If the value isn't\na number at all it throws `md::type_error`. Every other strict `as_*`\naccessor throws `std::bad_variant_access` on a type mismatch.\n\nThe pointer-returning `as_*_if()` family is `noexcept` and returns\n`nullptr` on a mismatch — use it when exceptions are not what you want:\n\n```cpp\nmd::Value v{42};\nif (auto* p = v.as_int_if()) {\n    // *p is a std::int64_t\n}\n\n// value_or\u003cT\u003e: returns the stored T if the alternative matches, otherwise\n// returns the fallback.\nstd::int64_t n = v.value_or\u003cstd::int64_t\u003e(0);\n```\n\nConstruction is engineered against the classic JSON-value pitfalls:\n\n- `Value{true}` stays a `bool`. The bool constructor is constrained with\n  `requires std::same_as\u003cB, bool\u003e`, so an `int` does not slip in as `bool`.\n- Signed integer types (`int`, `short`, `long`, …) route to `int64_t`.\n  Unsigned types (`unsigned`, `size_t`, …) route to `uint64_t`. Character\n  types (`char`, `wchar_t`, `char8_t`, `char16_t`, `char32_t`) are\n  excluded from both — they are not treated as integers.\n- `float` literals stay 32-bit; `double` and `long double` route to\n  `double` (64-bit).\n- `Value{\"abc\"}` is a string. The constructor template\n  `template \u003cclass T\u003e Value(T*) = delete;` shoots down arbitrary pointer\n  decay into `bool`.\n\n### `md::Object` (a.k.a. `md::Metadata`)\n\n`Object` wraps\n`std::unordered_map\u003cstd::string, Value, TransparentStringHash, std::equal_to\u003c\u003e\u003e`\nand exposes both the familiar map surface and a small set of metadata\nhelpers.\n\n```cpp\nmd::Object o{{\"name\", \"x\"}};\n\no[\"count\"] = 1;                            // implicit Value(int)\no.at(\"name\");                              // throws std::out_of_range on miss\nauto it = o.find(\"name\");                  // STL-style iterator return\no.insert_or_assign(\"count\", md::Value{2});\no.erase(\"count\");\nfor (const auto\u0026 [k, v] : o) { /* ... */ }\n```\n\nThe transparent hash and `equal_to\u003c\u003e` mean `find`, `contains`, `count`,\nand `erase` accept `std::string_view` and `const char*` directly without\nallocating a temporary `std::string`.\n\nMetadata helpers on `Object`:\n\n| Method                                          | Returns / throws                                       |\n|-------------------------------------------------|--------------------------------------------------------|\n| `contains(k)`                                   | `bool`                                                 |\n| `find_ptr(k)`                                   | `Value*` or `nullptr`                                  |\n| `require(k)`                                    | `Value\u0026`, or throws `missing_key_error`                |\n| `require_string(k)` / `_array` / `_object`      | typed reference, or `missing_key_error` / `type_error` |\n| `get_string_if(k)` / `_array_if` / `_object_if` | `const T*` or `nullptr` (never throws)                 |\n| `merge(src)`                                    | deep merge — source wins on conflict                   |\n| `find_path(\"a.b[0].c\")`                         | `Value*` or `nullptr`                                  |\n| `require_path(...)`                             | reference, or throws                                   |\n| `contains_path(...)`                            | `bool`                                                 |\n\nFree-function forwarders in `\u003cmd/helpers.hpp\u003e` (`md::contains(o, k)`,\n`md::require_string(o, k)`, etc.) exist for code that prefers a\nnon-method style.\n\n`Metadata` is a type alias for `Object` — the only reason to use the\nalias is for self-documenting names in user code.\n\n### `md::Array`\n\n`Array` is `std::vector\u003cValue\u003e`. There is no wrapper class around it, so\nall of the standard vector operations apply (push, emplace, iterators,\nrange-for, etc.). Two factory helpers exist for symmetry:\n\n```cpp\nmd::Array empty = md::array();\nmd::Array three = md::array({md::Value{1}, md::Value{2}, md::Value{3}});\n```\n\n### Path syntax\n\nPaths use `.` to descend into objects and `[N]` to index into arrays.\nThere is no escaping for `.` or `[` inside keys — keys with those\ncharacters cannot be addressed by path; use direct `o[\"...\"]` access\ninstead.\n\n```text\na.b.c        — walk three nested objects\nitems[0]     — index into the array under \"items\"\na[1].b       — array, then object\na[1][0]      — nested arrays\n\"\"           — empty path: see below\n```\n\nBehavior:\n\n- `find_path` returns `nullptr` for any of: missing key, out-of-range\n  index, malformed syntax (`items[`, `items[abc]`, leading `..`), and\n  type mismatches (descending into a non-object, indexing a non-array).\n- `require_path` throws `missing_key_error` for plain misses and\n  `type_error` for malformed syntax or type mismatches.\n- `contains_path` is the boolean equivalent of `find_path`.\n- An empty path returns `nullptr` / `false` and `require_path(\"\")` throws\n  `type_error`, because the API cannot return a `Value\u0026` to the root\n  object itself (the root is an `Object`, not a `Value`).\n\n### `merge`\n\nDeep merge with **source wins**:\n\n- If both sides are `Object`, recurse.\n- Otherwise, the source value overwrites the destination value. Arrays\n  are **replaced**, not concatenated.\n\nThis is fine for layered configuration and overlays. If you need\narray-append or any other strategy, do it explicitly with `as_array()`.\n\n### Streaming and `std::format`\n\n`\u003cmd/ostream.hpp\u003e` defines `operator\u003c\u003c` for all three types, and\n`\u003cmd/format.hpp\u003e` defines `std::formatter` specializations for the same\nthree. Both go through the same compact JSON writer:\n\n```cpp\nstd::cout  \u003c\u003c v \u003c\u003c '\\n';\nstd::string s = std::format(\"{}\", v);\n```\n\n- The format spec is **empty-only**. `std::format(\"{:p}\", v)` throws\n  `std::format_error`. Pretty-printing is out of scope for v1.\n- Strings are escaped to JSON: `\"`, `\\`, `\\b`, `\\f`, `\\n`, `\\r`, `\\t`,\n  and any other control character as `\\u00XX`.\n- Floating-point values use `std::to_chars` shortest round-trip, so the\n  output is the minimum number of decimal digits that reparse to the\n  exact same binary value.\n\n### Hashing\n\n`\u003cmd/hash.hpp\u003e` provides `std::hash` specializations for `Value`,\n`Object`, and `Array`. A few things worth knowing:\n\n- Per-alternative salt: `Value{0}`, `Value{0u}`, `Value{0.0f}`,\n  `Value{0.0}`, and `Value{false}` all hash to different values.\n- Arrays hash positionally (order matters).\n- **Objects hash with a commutative XOR fold of per-entry hashes**, so\n  two `Object`s that compare equal also hash equal regardless of\n  insertion order. This makes `Object` usable as a key in an\n  `unordered_*` container.\n- The hash is **stable within a single process only**. Do not persist\n  these hashes to disk or send them over a network expecting another\n  process to reproduce them.\n\n### Optional: nlohmann/json adapter\n\nIf `\u003cnlohmann/json.hpp\u003e` is on your include path, `\u003cmd/metadata.hpp\u003e`\nautomatically pulls in `\u003cmd/json.hpp\u003e`, which provides ADL hooks plus\nconvenience helpers:\n\n```cpp\n#include \u003cmd/metadata.hpp\u003e\n#include \u003cnlohmann/json.hpp\u003e          // include order doesn't matter\n\nmd::Object m{{\"k\", 1}};\n\n// ADL hooks let nlohmann's converters Just Work.\nnlohmann::json j = m;                            // -\u003e to_json(json\u0026, const Object\u0026)\nmd::Value     v = j.get\u003cmd::Value\u003e();            // -\u003e from_json(const json\u0026, Value\u0026)\n\n// Non-ADL convenience forms (slightly nicer at call sites).\nnlohmann::json j2 = md::to_json(m);\nmd::Value      v2 = md::from_json(j2);\n```\n\nInteger routing in `from_json` follows nlohmann's parser-level\ndiscrimination: `number_unsigned` → `uint64`, `number_integer` →\n`int64`. A non-negative integer parsed from JSON text may come back as\neither, depending on how nlohmann tokenized it. The test suite\naccordingly accepts `is_int() || is_uint()` for that case.\n\n**Float vs double does not survive a JSON round-trip.** nlohmann's\nparser collapses every floating-point value to its `number_float`\n(`double`) bucket. A `float` that goes out as JSON comes back as a\n`double`. This is documented in `tests/test_json.cpp::FloatRoundTripsThroughDouble`.\nThere is no workaround inside this library — pick a custom wire format\nif you need float fidelity.\n\n`md::from_json(j, Object\u0026)` throws `md::type_error` if `j` is not a JSON\nobject.\n\n### Optional: Parcel adapter\n\nIf [`Parcel`](https://github.com/aurimasniekis/cpp-parcel) is on your\ninclude path, `\u003cmd/metadata.hpp\u003e` automatically pulls in `\u003cmd/parcel.hpp\u003e`,\nwhich exposes the three core types as primitive-style parcel cells:\n\n| Cell             | Storage      | Wire kind |\n|------------------|--------------|-----------|\n| `md::ValueCell`  | `md::Value`  | `md:v`    |\n| `md::ObjectCell` | `md::Object` | `md:o`    |\n| `md::ArrayCell`  | `md::Array`  | `md:a`    |\n\nEach cell derives from `parcel::BaseCell\u003c…, Storage\u003e` and reuses the\nexisting `\u003cmd/json.hpp\u003e` ADL hooks for the inner JSON shape, so the\nserialized cell is the standard parcel `{\"k\", \"v\"}` envelope with the\nmd value already inside the `\"v\"` slot:\n\n```cpp\n#include \u003cmd/metadata.hpp\u003e          // also pulls in \u003cmd/parcel.hpp\u003e\n#include \u003cparcel/parcel.h\u003e\n\nparcel::ParcelRegistry registry;\nmd::register_cells(registry);   // shorthand for the three register_kind calls\n\nmd::Object payload{\n    {\"name\",     \"sensor\"},\n    {\"readings\", md::Array{1.0, 2.5, 3.75}},\n};\n\n// Wrap and serialize.\nmd::ObjectCell cell{payload};\nauto wire = cell.to_json();           // {\"k\":\"md:o\",\"v\":{\"name\":...}}\n\n// Round-trip back through the registry.\nparcel::cell_t restored = registry.cell_from_json(wire);\nauto* back = dynamic_cast\u003cmd::ObjectCell*\u003e(restored.get());\nconst md::Object\u0026 restored_obj = back-\u003evalue;\n```\n\n`PARCEL_DEFAULT_CELL` specializations are emitted for all three types,\nso `parcel::cell(md::Value{42})` (and the equivalents for `Object` /\n`Array`) automatically pick the right cell wrapper. A `FieldsBuilder`\nfield of type `md::Object` likewise infers `md::ObjectCell` without an\nexplicit `CellT` argument.\n\nThe same caveats apply as for the raw nlohmann/json adapter: floats\ncollapse to `double` on the way back from JSON, and `from_json` is\nstrict about the `\"k\"` tag matching the expected wire kind.\n\n---\n\n## Common usage patterns\n\n### Building a tree by assignment\n\n```cpp\nmd::Object o;\no[\"name\"]   = \"sensor\";   // const char*  -\u003e Value(string)\no[\"count\"]  = 42;         // int          -\u003e Value(int64)\no[\"weight\"] = 3.14;       // double       -\u003e Value(double)\no[\"ratio\"]  = 1.5f;       // float        -\u003e Value(float)  (stays float)\no[\"live\"]   = true;       // bool         -\u003e Value(bool)\n```\n\nUse braced lists for compound values. The two `operator=` overloads\ndisambiguate by element type — bare values pick the `Array` overload,\npair-shaped elements pick the `Object` overload:\n\n```cpp\no[\"tags\"] = {\"alpha\", \"beta\"};           // -\u003e Array\no[\"sub\"]  = {{\"k\", 1}, {\"k2\", 2}};       // -\u003e Object\n```\n\nSame disambiguation works inside an `Object{...}` constructor list:\n\n```cpp\nmd::Object root{\n    {\"name\",    \"default\"},\n    {\"options\", {{\"retries\", 3}, {\"timeout_ms\", 1000}}},  // nested Object\n    {\"tags\",    md::Array{\"a\", \"b\"}},                     // explicit Array\n};\n```\n\n### Reading values back out\n\nThere are three flavors, increasing in strictness:\n\n```cpp\n// 1) Pointer/noexcept form — best for \"I'm not sure\".\nif (auto* s = o.get_string_if(\"name\")) { std::cout \u003c\u003c *s \u003c\u003c '\\n'; }\n\n// 2) Strict typed form — throws on miss or type mismatch.\nconst std::string\u0026 name = o.require_string(\"name\");\n\n// 3) Path form — same throw/null behavior, deep access.\nconst md::Value\u0026 fw_major = o.require_path(\"device.firmware.major\");\nstd::cout \u003c\u003c fw_major.as_int() \u003c\u003c '\\n';\n```\n\n### Walking nested structures\n\n```cpp\nmd::Object root{\n    {\"device\",   {{\"id\", \"abc-123\"},\n                  {\"firmware\", {{\"major\", 1}, {\"minor\", 4}}}}},\n    {\"channels\", md::Array{\n        md::Object{{\"name\", \"left\"},  {\"gain_db\",  0.5}},\n        md::Object{{\"name\", \"right\"}, {\"gain_db\", -1.0}},\n    }},\n};\n\nconst md::Object\u0026 fw = root.require_object(\"device\").require_object(\"firmware\");\nstd::cout \u003c\u003c fw.at(\"major\").as_int() \u003c\u003c '.' \u003c\u003c fw.at(\"minor\").as_int() \u003c\u003c '\\n';\n\nfor (const md::Value\u0026 ch : root.require_array(\"channels\")) {\n    const md::Object\u0026 c = ch.as_object();\n    std::cout \u003c\u003c c.at(\"name\").as_string()\n              \u003c\u003c \" gain=\" \u003c\u003c c.at(\"gain_db\").as_double() \u003c\u003c '\\n';\n}\n```\n\nThis is also the recommended way to build an array of objects: use\n`md::Array{...}` for the outer container and `md::Object{...}` for each\nelement. A braced list of brace-pair-shaped elements would not\ndisambiguate at the outer level (see *Edge cases* below).\n\n### Layering configuration with `merge`\n\n```cpp\nmd::Object base{\n    {\"name\",    \"default\"},\n    {\"options\", {{\"retries\", 3}, {\"timeout_ms\", 1000}}},\n    {\"tags\",    md::Array{\"a\", \"b\"}},\n};\n\nconst md::Object overlay{\n    {\"options\",     {{\"timeout_ms\", 500}, {\"strict\", true}}},\n    {\"tags\",        md::Array{\"x\"}},     // arrays are replaced\n    {\"description\", \"overridden\"},\n};\n\nbase.merge(overlay);\n// base[\"options\"] is now {retries:3, timeout_ms:500, strict:true}\n// base[\"tags\"]    is now [\"x\"]\n// base[\"description\"] was inserted\n```\n\n### Path-style access\n\n```cpp\nmd::Object m{\n    {\"device\",   {{\"name\", \"acme\"}, {\"port\", 8080}}},\n    {\"channels\", md::Array{\n        md::Object{{\"freq_hz\", 2.4e9}},\n        md::Object{{\"freq_hz\", 5.8e9}},\n    }},\n};\n\nm.require_path(\"device.name\").as_string();         // \"acme\"\nm.require_path(\"channels[0].freq_hz\").as_double(); // 2.4e9\n\nif (const md::Value* p = m.find_path(\"device.absent\")) {\n    // not reached\n} else {\n    // missing key -\u003e nullptr, no exception\n}\n\nm.contains_path(\"channels[5]\");   // false  (out of range)\nm.contains_path(\"device.port\");   // true\n```\n\nMutating via path also works:\n\n```cpp\nmd::Value* p = m.find_path(\"device.port\");\nif (p != nullptr) {\n    *p = md::Value{9090};\n}\n```\n\n### Formatted output\n\n```cpp\nmd::Value v = md::Object{\n    {\"name\",      \"radio\"},\n    {\"power_dbm\", -10.5},\n    {\"channels\",  md::Array{1, 2, 3}},\n};\n\nstd::cout \u003c\u003c std::format(\"{}\", v) \u003c\u003c '\\n';\nstd::cout \u003c\u003c v \u003c\u003c '\\n';\n// Both: {\"name\":\"radio\",\"power_dbm\":-10.5,\"channels\":[1,2,3]}\n// (Key order is unspecified — see below.)\n```\n\n### Hashing into unordered containers\n\n```cpp\n#include \u003cunordered_set\u003e\n\nstd::unordered_set\u003cmd::Value\u003e seen;\nseen.insert(md::Value{42});\nseen.insert(md::Value{md::Object{{\"k\", 1}}});\nseen.contains(md::Value{42});   // true\n```\n\nThe `Object` hash is intentionally commutative so insertion order doesn't\nbreak the equality/hash contract.\n\n### Round-tripping through nlohmann/json\n\n```cpp\n#include \u003cnlohmann/json.hpp\u003e\n#include \u003cmd/metadata.hpp\u003e\n\nmd::Object m{\n    {\"name\",     \"sensor\"},\n    {\"enabled\",  true},\n    {\"readings\", md::Array{1.0, 2.5, 3.75}},\n};\n\nnlohmann::json j = md::to_json(m);\nstd::string text = j.dump();              // \"{...}\"\n\nmd::Value back = md::from_json(j);\n```\n\n---\n\n## Error handling\n\nThe library reports errors through exceptions. There are three\nproject-specific exception types, all under `namespace md`:\n\n```cpp\nstruct error             : std::runtime_error { /* ... */ };\nstruct missing_key_error : error              { /* ... */ };\nstruct type_error        : error              { /* ... */ };\n```\n\n| Mechanism                 | Thrown by                                                                                                                                                                  |\n|---------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `missing_key_error`       | `Object::require`, `require_*`, `require_path` on a missing key                                                                                                            |\n| `type_error`              | `require_*` on type mismatch; `require_path` on malformed path or type mismatch; `Value::as_double` on a non-number; `from_json(json, Object\u0026)` on a non-object JSON value |\n| `std::bad_variant_access` | Strict `as_bool` / `as_int` / `as_uint` / `as_float` / `as_string` / `as_array` / `as_object` on the wrong alternative                                                     |\n| `std::out_of_range`       | `Object::at` on a missing key                                                                                                                                              |\n| `std::format_error`       | A non-empty `std::format` spec on any of the supported types                                                                                                               |\n\nIf you want to avoid exceptions entirely, stay on the noexcept side of\nthe API: `as_*_if()`, `get_if\u003cT\u003e()`, `value_or\u003cT\u003e()`, `find_ptr()`,\n`find_path()`, `contains`, `contains_path`.\n\n```cpp\nmd::Value v;             // null\ntry {\n    (void)v.as_int();    // throws std::bad_variant_access\n} catch (const std::bad_variant_access\u0026) {\n    // ...\n}\n\nmd::Object o;\ntry {\n    (void)o.require(\"missing\");\n} catch (const md::missing_key_error\u0026 e) {\n    std::cout \u003c\u003c e.what() \u003c\u003c '\\n';\n}\n```\n\n---\n\n## Edge cases and pitfalls\n\nThese come straight from the implementation and tests — read them once\nand most of the surprises go away.\n\n### `Value{42}` is a scalar, never a one-element array\n\nThere is no `Value(std::initializer_list\u003cValue\u003e)` constructor — adding\none would silently change the meaning of every existing brace-init site.\nIf you want a one-element array, write `md::Array{42}`.\n\n### Nested-array literals don't work\n\n```cpp\nm[\"x\"] = {1, {2, 3}, 4};                     // does NOT compile cleanly\nm[\"x\"] = {1, md::Array{2, 3}, 4};            // OK\n```\n\nThe inner `{2, 3}` cannot form a `Value` for the reason above. Use\n`md::Array{}` explicitly for any nested array.\n\n### Empty `{}` is ambiguous\n\n```cpp\nm[\"x\"] = {};                  // ambiguous: Array? Object?\nm[\"x\"] = md::Array{};         // explicit empty Array\nm[\"x\"] = md::Object{};        // explicit empty Object\n```\n\nThe two `operator=` overloads on `Value` both match an empty list. Be\nexplicit.\n\n### Arrays of objects need an outer `md::Array{}`\n\n```cpp\nroot[\"channels\"] = md::Array{\n    md::Object{{\"name\", \"left\"}},\n    md::Object{{\"name\", \"right\"}},\n};\n```\n\nThis is the reliable form. A bare brace list of `Object`s would fall back\nto the pair-shaped overload and not compile.\n\n### `Object` keys are unordered\n\nInsertion order is not preserved — the backing container is\n`std::unordered_map`. Two `Object`s that compare equal may print their\nkeys in different orders, but they will hash equal and compare equal.\nIf you need stable text output, sort the keys yourself before printing.\n\n### Float fidelity is lost through JSON\n\nnlohmann's parser maps every floating-point JSON value to `double`. A\n`Value{1.5f}` round-tripped through nlohmann comes back as `Value{1.5}`\n(a `double`). See `tests/test_json.cpp::FloatRoundTripsThroughDouble`.\n\n### `as_float()` is strict, `as_double()` is not\n\n```cpp\nmd::Value d{2.25};      // double\n(void)d.as_float();     // throws std::bad_variant_access — strict\n(void)d.as_double();    // 2.25 — fine\n\nmd::Value i{42};        // int64\n(void)i.as_double();    // 42.0 — widens\n(void)i.as_int();       // 42  — strict, exact\n```\n\nIf you want a \"give me a number as a double regardless of how it's\nstored\", `as_double()` is the only one that does that.\n\n### Path syntax is dot+bracket only\n\nKeys containing `.` or `[` cannot be addressed by `find_path` / `require_path`.\nUse `Object::operator[]` or `find` to reach them. The path parser also\nrejects:\n\n- a leading `.` (`.a`)\n- two adjacent dots (`..a`)\n- an unclosed bracket (`items[`)\n- a non-digit inside brackets (`items[abc]`)\n- an empty bracket pair (`items[]`)\n\n### Path of `\"\"` returns null, not the root\n\n`find_path(\"\")` is `nullptr`, `contains_path(\"\")` is `false`,\n`require_path(\"\")` throws `type_error`. The root is an `Object`, not a\n`Value`, so there is nothing for the function to hand back.\n\n### Hashes are process-local\n\nDo not persist values returned by `std::hash\u003cmd::Value\u003e` (or `Object` /\n`Array`). The mixing constants are fine for an in-memory hash table only.\n\n### Format spec is empty-only\n\n`std::format(\"{:p}\", v)` throws `std::format_error`. There is no\npretty-printing option in v1.\n\n### Copying `Value` is a real copy\n\nA `Value` holding an `Object` stores it inside a `std::unique_ptr\u003cObject\u003e`\n(to break the recursive type), but copy construction deep-copies the\ncontained `Object`. Move construction is `noexcept`.\n\n---\n\n## API overview\n\nA compact map of the public surface a typical user touches. This is not\na full API dump — read the headers for the full list of overloads.\n\n| Symbol                                               | Purpose                                                            |\n|------------------------------------------------------|--------------------------------------------------------------------|\n| `md::Value`                                          | The variant type. Predicates + strict + noexcept accessors.        |\n| `md::Object` (`md::Metadata`)                        | The unordered-map-shaped container with metadata helpers.          |\n| `md::Array`                                          | Type alias for `std::vector\u003cmd::Value\u003e`.                           |\n| `md::null()`, `md::boolean(b)`                       | Factory helpers for the trivial values.                            |\n| `md::number\u003cT\u003e(x)`                                   | Factory helper constrained to numeric types.                       |\n| `md::string(...)`                                    | Factory helper from `std::string` / `string_view` / `const char*`. |\n| `md::array()`, `md::array({...})`                    | Array factory helpers.                                             |\n| `md::object()`, `md::object({...})`                  | Object factory helpers.                                            |\n| `Object::operator[]`, `at`, `find`, `contains`, …    | The `std::unordered_map` surface.                                  |\n| `Object::require[...]`, `get_*_if`                   | Typed retrieval with / without exceptions.                         |\n| `Object::find_path`, `require_path`, `contains_path` | Dot+bracket path access.                                           |\n| `Object::merge(src)`                                 | Deep merge, source wins.                                           |\n| `md::contains(o,k)`, `md::require_string(o,k)`, …    | Free-function forwarders for the helper methods.                   |\n| `operator\u003c\u003c`, `std::formatter\u003c...\u003e`                  | Compact JSON output (empty spec only).                             |\n| `std::hash\u003cValue/Object/Array\u003e`                      | Hashing for unordered containers.                                  |\n| `md::to_json`, `md::from_json`                       | nlohmann/json adapter (header `\u003cmd/json.hpp\u003e`).                    |\n| `md::error`, `missing_key_error`, `type_error`       | Exception hierarchy.                                               |\n\n---\n\n## Examples\n\nThe `examples/` directory contains short, runnable programs:\n\n| File                            | Demonstrates                                                 |\n|---------------------------------|--------------------------------------------------------------|\n| `examples/basic.cpp`            | Building a small `Metadata`, printing it, simple retrieval.  |\n| `examples/nested.cpp`           | Nested objects, arrays of objects, deep `require_*` access.  |\n| `examples/path_lookup.cpp`      | `require_path`, `find_path`, `contains_path` (hit and miss). |\n| `examples/merge.cpp`            | Deep merge with array replacement and new-key insertion.     |\n| `examples/format_output.cpp`    | `std::format` vs `operator\u003c\u003c` on the same value.             |\n| `examples/object_helpers.cpp`   | Method form vs free-function form, optional + strict access. |\n| `examples/json_integration.cpp` | Round-trip through `nlohmann::json`.                         |\n\nAll example programs are wired up in `examples/CMakeLists.txt`. When\n`METADATA_BUILD_EXAMPLES=ON` (the default at the top level), CMake builds\nthem as `metadata_basic`, `metadata_nested`, etc.\n\n---\n\n## Building and testing\n\nThe repository ships a thin `Makefile` over the CMake build for the\ncommon workflows. The CMake commands directly are equally fine:\n\n```bash\n# Configure + build + test\ncmake -S . -B build\ncmake --build build\nctest --test-dir build --output-on-failure\n```\n\nOr via the wrapper:\n\n```bash\nmake build          # configure + build in build/        (Debug)\nmake test           # ctest in build/\nmake examples       # build and run every metadata_* example, fail on non-zero exit\nmake sanitize       # Debug build + tests with ASan + UBSan in build-san/\nmake tidy           # Debug build with clang-tidy in build-tidy/\nmake release        # Release build + tests in build-release/\nmake no-json        # Build + test with METADATA_WITH_NLOHMANN_JSON=OFF\nmake coverage       # Clang source-based coverage in build-coverage/\nmake docs           # Doxygen HTML in build-docs/\nmake format         # clang-format -i over include / tests / examples\nmake ci             # format-check + tidy + test + sanitize + release + no-json\n```\n\nGoogleTest 1.17 is fetched automatically via `FetchContent` if no\n`GTest` package is found.\n\n### Build options\n\n| CMake option                  | Default        | Effect                                                                |\n|-------------------------------|----------------|-----------------------------------------------------------------------|\n| `METADATA_BUILD_TESTS`        | top-level only | Build the GoogleTest suite.                                           |\n| `METADATA_BUILD_EXAMPLES`     | top-level only | Build every example target.                                           |\n| `METADATA_BUILD_DOCS`         | `OFF`          | Configure the Doxygen target (`metadata_docs`).                       |\n| `METADATA_WITH_NLOHMANN_JSON` | `ON`           | Link `nlohmann/json` and define `METADATA_WITH_NLOHMANN_JSON=1`.      |\n| `METADATA_ENABLE_SANITIZERS`  | `OFF`          | ASan + UBSan flags (Debug).                                           |\n| `METADATA_ENABLE_CLANG_TIDY`  | `OFF`          | Run clang-tidy during the build.                                      |\n| `METADATA_ENABLE_COVERAGE`    | `OFF`          | Clang source-based coverage flags.                                    |\n| `METADATA_WARNINGS_AS_ERRORS` | top-level only | Promote compiler warnings to errors.                                  |\n| `METADATA_INSTALL`            | top-level only | Generate install rules; auto-disabled if `nlohmann_json` was fetched. |\n\n---\n\n## FAQ\n\n**Do I need to link anything?**\nNo. `metadata::metadata` is a CMake `INTERFACE` target — it adds the\ninclude directories and turns on C++23. The library is header-only.\n\n**Is it thread-safe?**\nNo more than `std::unordered_map\u003cstd::string, std::variant\u003c...\u003e\u003e` is.\nConcurrent reads of an unchanged `Object` are fine. Concurrent writes,\nor a write concurrent with any read, require external synchronization.\nHash functions are pure and noexcept.\n\n**Does `Value` own its data or borrow it?**\n`Value` owns its data. `Value(std::string_view)` and `Value(const char*)`\ncopy into a `std::string`. `Value(Object)` moves into an internal\n`std::unique_ptr\u003cObject\u003e` (the indirection breaks the recursive type\nrelationship between `Value` and `Object`).\n\n**Why is `Object` wrapped in a `unique_ptr` inside `Value`?**\nBecause `std::variant\u003c..., Object\u003e` would need `Object` complete at the\npoint where `Value` is defined — and `Object` itself stores `Value`s.\nThe `unique_ptr\u003cObject\u003e` indirection breaks the cycle and keeps the\npublic API normal-looking (you still write `Value{Object{...}}`).\n\n**Can I parse JSON text with this?**\nNo. There is no parser. Use nlohmann/json (or any other JSON parser),\nthen `md::from_json(json)` to import the result.\n\n**Can I keep insertion order?**\nNot in v1. The `Object` storage is `std::unordered_map`. An ordered\nvariant is listed as a possible future direction.\n\n**Why does my float become a double after JSON round-trip?**\nnlohmann's parser collapses all floating-point JSON to `double`. There\nis nothing this library can do about it on the parser side. See the\n*Edge cases* section.\n\n**Why does `std::format(\"{:p}\", v)` throw?**\nThe compact JSON writer is the only output mode in v1; the format-spec\nparser rejects any non-empty spec with `std::format_error`.\n\n**Why is `as_int()` strict but `as_double()` widening?**\n`as_double()` is intentionally the one accessor that papers over the\nint/uint/float/double distinction, for code that just wants \"a number as\na double\". Every other accessor is strict so that bugs are loud.\n\n**Does `Object::operator[]` create missing keys?**\nYes — same as `std::unordered_map`. It default-constructs a `Value`\n(which is `null`) under the missing key. If you don't want that, use\n`find`, `contains`, or `find_ptr` instead.\n\n\n## Contributing\n\nContributions to the library are welcome! If you encounter any issues or have suggestions for\nimprovements,\nplease feel free to submit a pull request or open an issue on the project's repository.\n\n## License\n\nThis project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faurimasniekis%2Fcpp-metadata","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faurimasniekis%2Fcpp-metadata","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faurimasniekis%2Fcpp-metadata/lists"}