Repository navigation
Idea: borrowed / zero-copy read-only view over a source buffer #5295
Description
Status: brainstorming, not a plan
This is an idea to move forward on, not a sanctioned TODO or a commitment. It is a hypothesis to investigate, measure, and discuss. It may turn out to be not worth it, conflict with the owning-value philosophy, or be better shipped as a companion rather than in-tree. Treat this as a starting point for discussion.
Motivation
Today the parser builds an owning basic_json: every object/array/string is a separate heap allocation (create<T>(), include/nlohmann/json.hpp:411), and every string is copied out of the input and unescaped into a string_t. For parse-and-read workloads (config, one-shot RPC decode, inspection) we pay N allocations plus a full copy of every string, then free it all again — even though the source buffer is valid for the whole operation.
This is the most common basis for "nlohmann is slower than simdjson / Boost.JSON" comparisons. A read-only view that references the source bytes would close much of that gap without changing basic_json at all.
Prior art / models
- simdjson On-Demand — forward-only, lazy, no DOM; aggressive iterator invalidation. Fastest, most rigid.
- Boost.JSON — still owning, but
string_viewinput +monotonic_resourcemake allocation cheap rather than absent. - Borrowed DOM — random-access tree whose scalars reference the source. Best fit for our users' mental model.
Sketch
A separate sibling type, e.g. json_view / basic_json_view — not a mode on basic_json, because the whole basic_json API assumes owning, mutating semantics (operator[] returning a mutable basic_json&, in-place push_back, …). A view offers only the read subset plus an escape hatch back to the owning world:
std::string_view src = load();
auto doc = json::view(src); // no string copies, no per-node heap alloc
std::string_view name = doc["name"].get<std::string_view>();
int age = doc["age"].get<int>(); // number parsed lazily on access
nlohmann::json owned = doc["profile"].materialize(); // opt into the owning DOMThe escaping problem (the crux)
"a\nb" in the source is 4 bytes that must become 3, so a string_view into the source is impossible for escaped strings. Proposed fast-path / fallback split: during the index pass, flag whether a string token contains any escape. If not (the common case) the value is a string_view into src; if it does, materialize just that one string into a small side arena. The type is then "borrowed unless it can't be," preserving the zero-copy win without lying about semantics.
Implementation notes
- Two indexing options: (a) reuse the existing SAX pass to build a compact tape/index of
(type, source_offset, length, child/next)records — one owningstd::vector<node>, no per-node heap alloc, no string copies; or (b) a simdjson-style structural-index tape. Option (a) is far less work and reuses the current parser almost verbatim. - Numbers should be stored as unparsed token spans, parsed on access. This is the same machinery needed by the lossless-number idea (see cross-reference below), so the two should be prototyped together.
materialize()deep-copies a subtree into an owningbasic_json.
Open questions
- Public API surface of the view subset, and how much of the iterator/
at/find/contains/value()API to mirror. - Lifetime/dangling story: the view is only valid while
srcis alive — how to make that hard to misuse (naming, docs, maybe astd::span/string_view-only constructor). - Does this live in-tree or as a companion header? Impact on compile time (cf. Ideas: exploring ways to reduce compilation time #5294).
- Interaction with binary formats (CBOR/MsgPack/…), where "the source bytes" are not JSON text.
Dependencies / interactions
- Shares the "keep the token, parse lazily" machinery with the lossless-number idea — build together.
- Orthogonal to
basic_json; no ABI impact on the existing type.
Activity
Add the node index and scanning primitives of json_view (internal)
Add json_document and json_view: parse, accept, types, materialize
Add json_document and json_view: node index, parser, and document
Add json_document and json_view: node index, parser, and document (#5620