nlohmann / nlohmann/json

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

Open
#5,295 0 comments 0 reactions 0 assignees View on GitHub

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_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_viewnot 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. #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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.