Idea: borrowed / zero-copy read-only view over a source buffer
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 50.6k
- Forks
- 7.5k
- Avg merge
- 4d 17h
- Merged PRs (30d)
- 58
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 DOM
The 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. #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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
No implementation entry point or settled API is specified. Start by reading include/nlohmann/json.hpp around create() and tracing the existing SAX parser, then compare the proposed tape/index approach with the lossless-number idea and #5294. Done means an agreed, measured design or prototype that resolves escaping, lifetime, materialization, and binary-format questions.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- backend-api-design, data
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100