Skip to content

Idea: borrowed / zero-copy read-only view over a source buffer #5295

Closed

Description

@nlohmann

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_view input + monotonic_resource make 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 owning std::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 owning basic_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 src is alive — how to make that hard to misuse (naming, docs, maybe a std::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.

Metadata

Metadata

Assignees

Labels

review neededIt would be great if someone could review the proposed changes.solution: proposed fixa fix for the issue has been proposed and waits for confirmation

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions