Skip to content

Add json_document and json_view: node index, parser, and document - #5620

Open
nlohmann wants to merge 2 commits into
json-view/23-zmijfrom
json-view/08-view-builder
Open

nlohmann wants to merge 2 commits into
json-view/23-zmijfrom
json-view/08-view-builder

Conversation

@nlohmann

@nlohmann nlohmann commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Part of the stack for the zero-copy view (#5295). This PR combines #5620 and #5621, which were reviewed separately before: the internal node index and parser, and the public json_document/json_view classes built on it. Element access, values, dump(), and comparisons follow in later PRs, so each one stays reviewable.

#include <nlohmann/json_view.hpp>

std::string text = receive();
auto doc = nlohmann::json_document::parse(text);   // borrows text
nlohmann::json_view root = doc.root();
if (root.is_object() && !root.empty())
{
    nlohmann::json j = root.materialize();          // the value json::parse(text) returns
}

Summary

  • Comments end at a NUL like in parse(): with comments enabled, a NUL byte that ends a // comment is the end of the input, as develop's lexer does since Keep a NUL byte ending a // comment as the end of input #5696; before, the view consumed it as part of the comment.
  • A parse produces a flat array of 16-byte nodes in document order: one node per value, and one per object key.
    • Strings stay in the source text, and escaped strings are decoded into an arena.
    • Integers are converted while their digits are in the cache.
    • Floats keep only their digit layout; they are converted when read, with the library's float converter from Speed up the lexer: own float parser, string scan, and \u table #5738 (independent of the locale, no allocation).
    • Containers store the size of their subtree, so a reader can step over one in constant time.
    • Nothing is allocated per value: a document makes a handful of allocations, however many values it has.
  • The parser accepts exactly what json::parse accepts: every combination of ignore_comments and ignore_trailing_commas, with and without a trailing NUL, and under JSON_STRICT_NUL_HANDLING. It is portable C++11, and nothing depends on the byte order: words are read in a fixed order, and literals are compared with memcmp. The SIMD kernels come later in the stack.
  • The parse state lives in a local cursor whose address never escapes, so it stays in registers; cold paths (errors, regrowth, escapes, comments) are out-of-line members.
  • basic_json_document<BasicJsonType>: parse, parse_copy, accept, read (reuses the document's memory), root, is_discarded, source, owns_source, node_count, memory_usage, shrink_to_fit.
  • basic_json_view<BasicJsonType>: type, the is_* queries, operator bool, size, empty, materialize, source_offset.
  • Aliases: json_document, json_view, ordered_json_document, ordered_json_view.
  • Input: borrowed (lvalue contiguous byte containers, std::string_view, C strings, character arrays, pointer ranges, and in C++20 other contiguous iterator ranges) or owned (an rvalue std::string, moved in, and anything else parse() accepts, copied or read).
  • Errors: a parse error throws the exception basic_json::parse would throw for the same input. The library parser runs on the failing input (a cold path), so message, position, and id match. Inputs of 4 GiB or more are rejected with the new out_of_range.416.
  • materialize(): builds the value with the SAX handler parse() uses, so JSON_DIAGNOSTICS parent pointers are set as usual.
  • detail::abi_config keeps JSON_STRICT_NUL_HANDLING and JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON readable after json.hpp undefines them at its end, in the ABI namespace, which already encodes both settings, so they always match the basic_json in use.

Performance

json_view

Measured on the regrouped stack

Measured on the regrouped stack (µs, best of 3 interleaved rounds of 15 runs; files from nativejson-benchmark). Apple M1 Max with Apple clang at -O2; x86-64 on a KVM Haswell VPS, pinned to one core, GCC 13 / Clang 18 at -O2 (the VPS is noisy, about ±5–10%).

json_document::parse vs json::parse (both at this PR):

Apple M1 x86-64 GCC x86-64 Clang
canada 1012 vs 7633 (7.5×) 1991 vs 14672 (7.4×) 2239 vs 15589 (7.0×)
citm_catalog 390 vs 2961 (7.6×) 877 vs 7646 (8.7×) 929 vs 6722 (7.2×)
twitter 244 vs 1413 (5.8×) 538 vs 4027 (7.5×) 508 vs 4024 (7.9×)

Earlier measurements (on the old stack)

json_document::parse vs json::parse: 3.1-6.0x faster (twitter 0.24 vs 0.80 ms, canada 1.03 vs 6.16 ms). Parse plus materialize() is still 1.4-2.1x faster than json::parse.

Apple M1, Google Benchmark, median of 5:

file json::parse json_document::parse speedup json::accept json_document::accept speedup
jeopardy 151.7 ms 26.1 ms 5.8x 82.0 ms 26.1 ms 3.1x
canada 9.18 ms 1.03 ms 8.9x 6.16 ms 1.03 ms 6.0x
citm_catalog 3.13 ms 0.42 ms 7.5x 2.03 ms 0.42 ms 4.9x
twitter 1.48 ms 0.24 ms 6.1x 0.80 ms 0.24 ms 3.3x

materialize() of the whole document: 80.1, 4.65, 1.06, and 0.72 ms for the same four files.

Core library (json::parse / dump)

Unaffected: this PR only adds internal code under include/nlohmann/detail/view/ and the new public header json_view.hpp. json.hpp only gains the internal detail::abi_config constants and the one include. No existing code path changes.

Tests

  • unit-json_view_builder.cpp: 140,882 assertions comparing the node index with json::parse for handwritten, generated, and damaged documents under all option combinations; input from std::string and from exact-size buffers with no read past the input under AddressSanitizer; deep nesting up to 100,000 levels; NUL, BOM, and whitespace cases; and the test-suite files.
  • unit-json_view.cpp: materialize() equals parse() for handwritten and generated documents, with duplicate keys, 100,000 levels of nesting, and JSON_DIAGNOSTICS parents; parse errors equal parse()'s, message included, for malformed inputs under all option combinations; every input kind, borrowed or owned; reuse with read(), moves, shrink_to_fit(), and source offsets.
  • unit-json_view_macros.cpp: includes the header without JSON_TEST_KEEP_MACROS, as users do, and checks that the view relies on no macro json.hpp undefines and leaks none of its own NLOHMANN_VIEW_* macros.
  • Fuzzer (fuzzer-parse_json_view.cpp): accept, value, and exception parity with json::parse.

Public API

New, additive API; nothing existing changes:

  • New header <nlohmann/json_view.hpp> with basic_json_document, basic_json_view, and the aliases json_document, json_view, ordered_json_document, ordered_json_view.
  • New exception id out_of_range.416 (input of 4 GiB or more).
  • The C++20 module exports the new names.
  • json.hpp gains the internal detail::abi_config constants (not part of the public API).
  • A new label aspect: json_view is used by labeler.yml and has to be created in the repository.

Written by Claude Code.

🤖 Generated with Claude Code

@nlohmann
nlohmann added this pull request to stack #5636 September 29, 2026 14:20
@nlohmann nlohmann changed the title json view/08 view builder Add the node index and parser of json_view (internal) Sep 29, 2026
@nlohmann

Copy link
Copy Markdown
Owner Author

CI fixes for this PR (commits 87a041a and ab4f646):

  • msvc (Debug, Win32, default): C4127 (conditional expression is constant) for TrailingCommas && cur() == ']', ... == '}' and Comments && cur() == '/' in detail/view/builder.hpp when the option is off. The template arguments now go through a static enabled(bool) function, the same approach as nesting_depth_exhausted() in json.hpp. Not reproduced locally (no MSVC here).
  • ci_test_single_header: unit-json_view_builder.cpp includes nlohmann/detail/view/*.hpp, which is not in single_include/. At this point in the stack, the test is built only with multiple headers. From Add json_document and json_view: parse, accept, types, materialize #5621 on, it includes <nlohmann/json_view.hpp> in single-header mode instead.
  • ci_test_gcc (not visible before, because the build stopped at an earlier error): -Werror=useless-cast on static_cast<std::size_t>(guess + (guess / 4) + 64) in builder::grow(), since std::uint64_t is std::size_t on Linux x86-64. The sum is now cast through a named variable. GCC does not report a cast of an lvalue.
  • The remaining GCC failures were inherited from Convert floats with Eisel-Lemire when std::from_chars is unavailable #5617/Find the stop byte of a string run without a byte loop #5618.

Verified with the CI's own image gcc:16 (linux/amd64) and the CI warning flags, for C++11 and C++20: the builder test builds and passes. A CMake configure with -DJSON_MultipleHeaders=OFF no longer lists the builder test.

— posted by Claude Code on behalf of @nlohmann

Comment thread include/nlohmann/detail/view/builder.hpp Dismissed
Comment thread include/nlohmann/detail/view/builder.hpp Dismissed
Comment thread include/nlohmann/detail/view/builder.hpp Dismissed
Comment thread include/nlohmann/detail/view/builder.hpp Dismissed
Comment thread include/nlohmann/detail/view/builder.hpp Dismissed
Comment thread include/nlohmann/detail/view/document_data.hpp Dismissed
Comment thread include/nlohmann/detail/view/node.hpp Dismissed
Comment thread include/nlohmann/detail/view/node.hpp Dismissed
Comment thread include/nlohmann/detail/view/scan.hpp Dismissed
Comment thread include/nlohmann/detail/view/string_ref.hpp Fixed
@github-actions github-actions Bot added the CMake label Sep 30, 2026
@nlohmann
nlohmann removed this pull request from stack #5636 September 30, 2026 13:19
@nlohmann
nlohmann force-pushed the json-view/08-view-builder branch from ab4f646 to dc9899f Compare September 30, 2026 13:20
@nlohmann
nlohmann added this pull request to stack #5739 September 30, 2026 13:21
@nlohmann
nlohmann force-pushed the json-view/08-view-builder branch 3 times, most recently from 7030188 to c195988 Compare September 30, 2026 18:06
@nlohmann
nlohmann marked this pull request as ready for review September 30, 2026 18:18
@nlohmann
nlohmann force-pushed the json-view/08-view-builder branch from c195988 to e96e298 Compare September 30, 2026 18:19
@nlohmann
nlohmann removed this pull request from stack #5739 October 6, 2026 09:26
@nlohmann
nlohmann force-pushed the json-view/08-view-builder branch from 2739c0b to 4303ba6 Compare October 6, 2026 09:28
@nlohmann
nlohmann changed the base branch from json-view/04-unicode-escapes to json-view/23-zmij October 6, 2026 09:29
@nlohmann nlohmann changed the title Add the node index and parser of json_view (internal) Add json_document and json_view: node index, parser, and document Oct 6, 2026
@nlohmann
nlohmann added this pull request to stack #5768 October 6, 2026 09:29
Add json_document and json_view, a read-only, zero-copy index of a
JSON text, as the first public slice of the zero-copy view (#5295).

A parse produces a flat array of 16-byte nodes in document order,
one per value and one per object key. Strings stay in the source
text; escaped strings are decoded into an arena. Integers are
converted while their digits are in the cache; floats keep only
their digit layout and are converted on read. Containers store the
size of their subtree, so a reader can step over one in constant
time. A document makes a handful of allocations, however many
values it has.

The parser accepts exactly what json::parse accepts, with every
combination of ignore_comments and ignore_trailing_commas, with and
without a trailing NUL, and under JSON_STRICT_NUL_HANDLING. It is
portable C++11 and does not depend on byte order.

basic_json_document adds parse, parse_copy, accept, read (reuses a
document's memory), root, is_discarded, source, owns_source,
node_count, memory_usage, and shrink_to_fit. basic_json_view adds
type, the is_* queries, operator bool, size, empty, materialize,
and source_offset. A parse error throws the same exception
basic_json::parse would throw for the same input, message and
position included.

detail::abi_config keeps JSON_STRICT_NUL_HANDLING and
JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON readable after json.hpp
undefines them, in the ABI namespace so they always match the
basic_json in use.

A NUL byte that ends a // comment is the end of the input, as in
parse() since #5696.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
@nlohmann
nlohmann force-pushed the json-view/08-view-builder branch from 4303ba6 to 759dd2e Compare October 7, 2026 14:44
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

🔴 Amalgamation check failed! 🔴

The source code has not been amalgamated and/or formatted correctly, or BUILD.bazel is out of date.

📎 A ready-to-apply patch is attached to the failed workflow run as the amalgamation-patch artifact. Download it, then apply it locally from the repository root with:

git apply amalgamation.patch

This does not require installing astyle yourself.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants