Document json_document and json_view

- API pages for basic_json_document and basic_json_view, one per member,
  and for the four aliases, each with an example
- features/json_view.md: the problem the view solves, ownership and
  lifetime, what matches basic_json::parse() and what differs, and when
  to choose json, ordered_json, SAX, or the view
- the examples show why one would use the view, not only how: borrowed
  vs. owned input, reading a few fields and materializing one subtree,
  reusing a document across many messages
- registered in the mkdocs navigation, llms.txt, the docset, the
  exceptions page (out_of_range.416), architecture.md, the integration
  page, and the README; the yyjson credit is added to the README and
  license.md

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann
2026-09-30 20:58:44 +02:00
parent 2f7d2548e1
commit 061c30310e
109 changed files with 2983 additions and 1 deletions
@@ -0,0 +1,19 @@
#include <iostream>
#include <nlohmann/json.hpp>
#include <nlohmann/json_view.hpp>
using json_document = nlohmann::json_document;
int main()
{
std::cout << std::boolalpha;
// accept() behaves exactly like basic_json::accept(): the same inputs are
// accepted or rejected, with the same ignore_comments/ignore_trailing_commas
// options
std::cout << json_document::accept(R"({"a": 1})") << '\n';
std::cout << json_document::accept(R"({"a": 1,})") << '\n'; // trailing comma: rejected by default
std::cout << json_document::accept(R"({"a": 1,})", false, true) << '\n'; // ignore_trailing_commas
std::cout << (json_document::accept(R"({"a": 1})") == nlohmann::json::accept(R"({"a": 1})")) << '\n';
}
@@ -0,0 +1,4 @@
true
false
true
true
@@ -0,0 +1,23 @@
#include <iostream>
#include <utility>
#include <nlohmann/json_view.hpp>
using json_document = nlohmann::json_document;
int main()
{
std::cout << std::boolalpha;
// the default constructor creates an empty (discarded) document
json_document empty;
std::cout << empty.is_discarded() << '\n';
// json_document is move-only: parse() itself returns by value (moved out),
// and a document can be moved again, e.g. into a container
json_document doc = json_document::parse(R"({"a": 1})");
json_document moved = std::move(doc);
std::cout << moved.root().is_object() << '\n';
// copying is disabled at compile time:
// json_document another = moved; // does not compile
}
@@ -0,0 +1,2 @@
true
true
@@ -0,0 +1,19 @@
#include <iostream>
#include <nlohmann/json_view.hpp>
using json_document = nlohmann::json_document;
int main()
{
std::cout << std::boolalpha;
// with allow_exceptions == false, a parse error produces a discarded
// document instead of throwing -- exactly like basic_json::parse()
json_document doc = json_document::parse(R"({"a": )", /* allow_exceptions */ false);
std::cout << doc.is_discarded() << '\n';
std::cout << doc.root().is_discarded() << '\n';
// a successful parse is never discarded
json_document ok = json_document::parse(R"({"a": 1})", false);
std::cout << ok.is_discarded() << '\n';
}
@@ -0,0 +1,3 @@
true
true
false
@@ -0,0 +1,28 @@
#include <iostream>
#include <string>
#include <nlohmann/json_view.hpp>
using json_document = nlohmann::json_document;
int main()
{
std::cout << std::boolalpha;
// long enough that copying it needs a real (heap) allocation, so the
// comparison below does not depend on the standard library's small
// string optimization threshold
const std::string text = std::string(200, ' ') + "[1, 2, 3, 4, 5]";
// memory_usage() is not portable across platforms/allocators/compilers, so
// compare it relatively instead of printing the raw byte count
json_document borrowed = json_document::parse(text);
json_document owned = json_document::parse_copy(text);
// the owned document additionally stores its own copy of the source text
std::cout << (owned.memory_usage() > borrowed.memory_usage()) << '\n';
// a document with more values needs a larger index
json_document small = json_document::parse(std::string("[1]"));
json_document large = json_document::parse(std::string("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"));
std::cout << (large.memory_usage() > small.memory_usage()) << '\n';
}
@@ -0,0 +1,2 @@
true
true
@@ -0,0 +1,15 @@
#include <iostream>
#include <nlohmann/json_view.hpp>
using json_document = nlohmann::json_document;
int main()
{
// node_count() is the size of the flat index: one 16-byte node per value,
// plus one per object key (values and keys are all the index stores)
json_document scalar = json_document::parse("42");
std::cout << scalar.node_count() << '\n';
json_document doc = json_document::parse(R"({"a": 1, "b": [1, 2]})");
std::cout << doc.node_count() << '\n';
}
@@ -0,0 +1,2 @@
1
7
@@ -0,0 +1,24 @@
#include <iostream>
#include <string>
#include <nlohmann/json_view.hpp>
using json_document = nlohmann::json_document;
int main()
{
std::cout << std::boolalpha;
std::string text = R"({"a": 1})";
// borrowed: the document only points into `text`; `text` must outlive it
json_document borrowed = json_document::parse(text);
std::cout << borrowed.owns_source() << '\n';
// owned: parse_copy() always takes its own copy
json_document copied = json_document::parse_copy(text);
std::cout << copied.owns_source() << '\n';
// owned: an rvalue std::string is moved in, not copied, but still owned
json_document moved_in = json_document::parse(std::string(text));
std::cout << moved_in.owns_source() << '\n';
}
@@ -0,0 +1,3 @@
false
true
true
@@ -0,0 +1,33 @@
#include <iostream>
#include <string>
#include <nlohmann/json.hpp>
#include <nlohmann/json_view.hpp>
using json_document = nlohmann::json_document;
int main()
{
std::cout << std::boolalpha;
// an lvalue std::string is BORROWED: the document only stores a pointer
// into `text`, so `text` must outlive `borrowed`
std::string text = R"({"count": 3})";
json_document borrowed = json_document::parse(text);
std::cout << borrowed.owns_source() << '\n'; // false
// an rvalue std::string is MOVED into the document -- no copy of the text
json_document owned = json_document::parse(std::string(R"({"count": 3})"));
std::cout << owned.owns_source() << '\n'; // true
// errors are identical to basic_json::parse: same exception id, message,
// and position, because the library parser runs on the same bytes on a
// failing input
try
{
static_cast<void>(json_document::parse(R"({"count": )"));
}
catch (const nlohmann::json::parse_error& e)
{
std::cout << e.id << '\n';
}
}
@@ -0,0 +1,3 @@
false
true
101
@@ -0,0 +1,22 @@
#include <iostream>
#include <nlohmann/json_view.hpp>
using json_document = nlohmann::json_document;
json_document parse_from_temporary_buffer()
{
char buffer[] = R"({"a": 1})";
// parse() would borrow `buffer`, which is about to go out of scope;
// parse_copy() takes its own copy instead, so the returned document does
// not depend on `buffer` afterward
return json_document::parse_copy(buffer);
}
int main()
{
std::cout << std::boolalpha;
json_document doc = parse_from_temporary_buffer();
std::cout << doc.owns_source() << '\n';
std::cout << doc.root().is_object() << '\n';
}
@@ -0,0 +1,2 @@
true
true
@@ -0,0 +1,22 @@
#include <iostream>
#include <vector>
#include <nlohmann/json_view.hpp>
using json_document = nlohmann::json_document;
int main()
{
std::cout << std::boolalpha;
// a JSON value embedded in a larger, non-null-terminated buffer (e.g. a
// slice received over the network)
std::vector<char> buffer = {'[', '1', ',', '2', ']', 'j', 'u', 'n', 'k'};
// a pointer pair is BORROWED, exactly like a byte container lvalue: the
// document points into the buffer. (From C++20 on, std::vector<char>
// iterators are borrowed as well; before, they are copied.)
const char* first = buffer.data();
json_document doc = json_document::parse(first, first + 5);
std::cout << doc.root().is_array() << ' ' << doc.root().size() << '\n';
std::cout << doc.owns_source() << '\n'; // false: borrowed
}
@@ -0,0 +1,2 @@
true 2
false
@@ -0,0 +1,31 @@
#include <iostream>
#include <string>
#include <vector>
#include <nlohmann/json_view.hpp>
using json_document = nlohmann::json_document;
int main()
{
std::cout << std::boolalpha;
std::vector<std::string> messages =
{
R"({"id": 1})", R"({"id": 2, "tag": "x"})", R"({"id": 3})"
};
// parse into the same document over and over: its node index and decode
// buffer are reused instead of being freed and reallocated for each message
json_document doc;
std::size_t total = 0;
for (const auto& msg : messages)
{
doc.read(msg);
total += doc.root().size();
}
std::cout << total << '\n';
// read() can also change what kind of input is owned/borrowed between calls
doc.read(std::string(R"({"owned": true})"));
std::cout << doc.owns_source() << '\n';
}
@@ -0,0 +1,2 @@
4
true
@@ -0,0 +1,19 @@
#include <iostream>
#include <nlohmann/json_view.hpp>
using json_document = nlohmann::json_document;
int main()
{
std::cout << std::boolalpha;
json_document doc = json_document::parse(R"({"greeting": "hi"})");
std::cout << doc.root().is_object() << '\n';
// root() is a cheap handle, not a copy: repeated calls observe the same value
std::cout << (doc.root().type() == doc.root().type()) << '\n';
// the root of a failed parse (allow_exceptions == false) is discarded
json_document failed = json_document::parse("{", false);
std::cout << failed.root().is_discarded() << '\n';
}
@@ -0,0 +1,3 @@
true
true
true
@@ -0,0 +1,44 @@
#include <iostream>
#include <sstream>
#include <string>
#include <nlohmann/json_view.hpp>
using json_document = nlohmann::json_document;
int main()
{
std::cout << std::boolalpha;
// build a large array (many nodes), then read a small one into the same
// document: the index grown for the large input is still allocated
std::ostringstream big;
big << '[';
for (int i = 0; i < 500; ++i)
{
if (i != 0)
{
big << ',';
}
big << i;
}
big << ']';
json_document doc;
doc.read(big.str());
const std::size_t big_nodes = doc.node_count();
doc.read(std::string("[1]"));
std::cout << (doc.node_count() < big_nodes) << '\n'; // far fewer live nodes now
const std::size_t before = doc.memory_usage();
// shrink_to_fit() moves the index into a block sized for what is actually
// used. This INVALIDATES every view taken from this document before the
// call (they point into the old, now-freed block) -- take fresh ones from
// root() afterward.
doc.shrink_to_fit();
const std::size_t after = doc.memory_usage();
std::cout << (after <= before) << '\n';
// a freshly taken view is valid and correct
std::cout << doc.root().is_array() << ' ' << doc.root().size() << '\n';
}
@@ -0,0 +1,3 @@
true
true
true 1
@@ -0,0 +1,20 @@
#include <iostream>
#include <string>
#include <nlohmann/json_view.hpp>
using json_document = nlohmann::json_document;
int main()
{
std::cout << std::boolalpha;
std::string text = R"({"a": 1})";
json_document doc = json_document::parse(text);
// source() is the parsed text, whether borrowed or owned
std::cout << (doc.source().size() == text.size()) << '\n';
std::cout << std::string(doc.source().data(), doc.source().size()) << '\n';
// for a borrowed document, source() points right into the caller's buffer
std::cout << (doc.source().data() == text.data()) << '\n';
}
@@ -0,0 +1,3 @@
true
{"a": 1}
true
@@ -0,0 +1,17 @@
#include <iostream>
#include <nlohmann/json_view.hpp>
int main()
{
std::cout << std::boolalpha;
// the default constructor is the only public one: it creates an invalid
// (discarded) view, useful as a "no value yet" placeholder
nlohmann::json_view v;
std::cout << static_cast<bool>(v) << ' ' << v.is_discarded() << '\n';
// views are trivially copyable handles (two pointers); the document owns
// the actual data
nlohmann::json_view copy = v;
std::cout << static_cast<bool>(copy) << '\n';
}
@@ -0,0 +1,2 @@
false true
false
@@ -0,0 +1,33 @@
#include <array>
#include <iostream>
#include <nlohmann/json_view.hpp>
using json_document = nlohmann::json_document;
int main()
{
// three incoming messages; skip the ones that are not useful without ever
// building a nlohmann::json value for them
json_document heartbeat = json_document::parse("null");
json_document empty_batch = json_document::parse("[]");
json_document batch = json_document::parse(R"([{"id": 1}, {"id": 2}, {"id": 3}])");
std::array<const json_document*, 3> messages = {{&heartbeat, &empty_batch, &batch}};
for (const json_document* d : messages)
{
// is_array()/empty() only look at the flat index: a discarded
// heartbeat or an empty batch is never turned into a nlohmann::json
// value, so no per-element allocation happens for them
if (!d->root().is_array() || d->root().empty())
{
std::cout << "skipped\n";
continue;
}
// materialize() replays the subtree through the same SAX builder
// basic_json::parse() uses, so the result is exactly what
// basic_json::parse() would have produced for the same text
nlohmann::json value = d->root().materialize();
std::cout << value.dump() << '\n';
}
}
@@ -0,0 +1,3 @@
skipped
skipped
[{"id":1},{"id":2},{"id":3}]
@@ -0,0 +1,22 @@
#include <iostream>
#include <nlohmann/json_view.hpp>
using json_document = nlohmann::json_document;
int main()
{
std::cout << std::boolalpha;
// decide whether a batch is worth processing before building any
// nlohmann::json value for it
json_document batch = json_document::parse(R"([1, 2, 3, 4, 5])");
json_document empty_batch = json_document::parse("[]");
std::cout << batch.root().empty() << ' ' << batch.root().size() << '\n';
std::cout << empty_batch.root().empty() << ' ' << empty_batch.root().size() << '\n';
// as for basic_json: null has size 0, every other scalar has size 1
json_document n = json_document::parse("null");
json_document s = json_document::parse(R"("hi")");
std::cout << n.root().size() << ' ' << s.root().size() << '\n';
}
@@ -0,0 +1,3 @@
false 5
true 0
0 1
@@ -0,0 +1,33 @@
#include <iostream>
#include <string>
#include <nlohmann/json_view.hpp>
using json_document = nlohmann::json_document;
int main()
{
std::cout << std::boolalpha;
// source_offset() points into source(): useful to report *where* in the
// original text a value came from (error messages, syntax highlighting,
// forwarding a sub-range verbatim, ...) without materializing it
json_document doc = json_document::parse(R"( 123)");
auto v = doc.root();
std::cout << v.source_offset() << ' '
<< std::string(doc.source().data() + v.source_offset(), 3) << '\n';
// a string without escapes also stays in the source text
json_document plain = json_document::parse(R"("ab")");
std::cout << plain.source()[plain.root().source_offset()] << '\n';
// a string with escapes is decoded once into the document's own buffer, so
// there is no single byte range in source() to point at: source_offset()
// returns the "not applicable" sentinel
json_document escaped = json_document::parse(R"("a\nb")");
std::cout << (escaped.root().source_offset() == static_cast<std::size_t>(-1)) << '\n';
// a discarded view -- default-constructed, or the root of a failed parse
// with allow_exceptions == false -- has no offset either
json_document failed = json_document::parse("not json", false);
std::cout << (failed.root().source_offset() == static_cast<std::size_t>(-1)) << '\n';
}
@@ -0,0 +1,4 @@
2 123
a
true
true
@@ -0,0 +1,43 @@
#include <iostream>
#include <nlohmann/json_view.hpp>
using json_document = nlohmann::json_document;
int main()
{
std::cout << std::boolalpha;
// several incoming messages, one json_document per message. type() and
// is_*() only look at the flat index built by parse(); no nlohmann::json
// tree exists yet, and none is built unless materialize() is called
json_document d_null = json_document::parse("null");
json_document d_bool = json_document::parse("true");
json_document d_int = json_document::parse("-42");
json_document d_unsigned = json_document::parse("42");
json_document d_float = json_document::parse("4.2");
json_document d_string = json_document::parse(R"("hi")");
json_document d_array = json_document::parse("[1, 2, 3]");
json_document d_object = json_document::parse(R"({"a": 1})");
std::cout << d_null.root().is_null() << '\n';
std::cout << d_bool.root().is_boolean() << '\n';
std::cout << d_int.root().is_number() << ' ' << d_int.root().is_number_integer() << '\n';
std::cout << d_unsigned.root().is_number_unsigned() << '\n';
std::cout << d_float.root().is_number_float() << '\n';
std::cout << d_string.root().is_string() << '\n';
std::cout << d_array.root().is_array() << ' ' << d_array.root().is_structured() << '\n';
std::cout << d_object.root().is_object() << ' ' << d_object.root().is_primitive() << '\n';
// JSON text can never produce a binary value: is_binary() is always false
std::cout << d_array.root().is_binary() << '\n';
// a default-constructed view, and the root of a document that failed to
// parse without exceptions, are both discarded
nlohmann::json_view invalid;
json_document failed = json_document::parse("not json", /* allow_exceptions */ false);
std::cout << static_cast<bool>(invalid) << ' ' << invalid.is_discarded() << '\n';
std::cout << static_cast<bool>(failed.root()) << ' ' << failed.root().is_discarded() << '\n';
// type() returns the same value_t enumeration as basic_json::type()
std::cout << (d_object.root().type() == nlohmann::json::value_t::object) << '\n';
}
@@ -0,0 +1,12 @@
true
true
true true
true
true
true
true true
true false
false
false true
false true
true
@@ -0,0 +1,16 @@
#include <iostream>
#include <nlohmann/json_view.hpp>
using json_document = nlohmann::json_document;
int main()
{
std::cout << std::boolalpha;
// json_document is basic_json_document<nlohmann::json>: same value types
// and containers as the ordinary json specialization
json_document doc = json_document::parse(R"({"pi": 3.14, "numbers": [1, 2, 3]})");
std::cout << doc.root().is_object() << '\n';
std::cout << doc.root().materialize().dump() << '\n';
}
@@ -0,0 +1,2 @@
true
{"numbers":[1,2,3],"pi":3.14}
+14
View File
@@ -0,0 +1,14 @@
#include <iostream>
#include <nlohmann/json_view.hpp>
int main()
{
std::cout << std::boolalpha;
// json_view is basic_json_view<nlohmann::json>: a read-only handle
// returned by json_document::root()
nlohmann::json_document doc = nlohmann::json_document::parse("[1, 2, 3]");
nlohmann::json_view v = doc.root();
std::cout << v.is_array() << ' ' << v.size() << '\n';
}
@@ -0,0 +1 @@
true 3
@@ -0,0 +1,33 @@
#include <iostream>
#include <string>
#include <utility>
#include <nlohmann/json_view.hpp>
using json_document = nlohmann::json_document;
int main()
{
std::cout << std::boolalpha;
// BORROWED: doc only points into `text`; `text` must outlive `doc`
std::string text = R"({"a": 1})";
json_document doc = json_document::parse(text);
std::cout << doc.owns_source() << '\n'; // false
// a view is valid as long as the document is alive, has not been
// re-parsed (read()) or shrunk (shrink_to_fit()), and -- if borrowed --
// the source text is alive
nlohmann::json_view v = doc.root();
std::cout << v.is_object() << '\n';
// re-parsing the SAME document invalidates views taken before the call;
// `v` above must not be used after this line
doc.read(R"([1, 2, 3])");
v = doc.root(); // take a fresh view instead
std::cout << v.is_array() << '\n';
// moving the document does not invalidate views: the node index is
// heap-allocated and does not move with the document object
json_document moved = std::move(doc);
std::cout << v.is_array() << '\n';
}
@@ -0,0 +1,4 @@
false
true
true
true
@@ -0,0 +1,13 @@
#include <iostream>
#include <nlohmann/json_view.hpp>
using ordered_json_document = nlohmann::ordered_json_document;
int main()
{
// ordered_json_document is basic_json_document<nlohmann::ordered_json>:
// materialize() preserves the insertion (source) order of object keys,
// instead of sorting them like json_document does
ordered_json_document doc = ordered_json_document::parse(R"({"z": 1, "a": 2, "m": 3})");
std::cout << doc.root().materialize().dump() << '\n';
}
@@ -0,0 +1 @@
{"z":1,"a":2,"m":3}
@@ -0,0 +1,10 @@
#include <iostream>
#include <nlohmann/json_view.hpp>
int main()
{
nlohmann::ordered_json_document doc = nlohmann::ordered_json_document::parse(R"({"z": 1, "a": 2})");
nlohmann::ordered_json_view v = doc.root();
std::cout << std::boolalpha << v.is_object() << ' ' << v.size() << '\n';
}
@@ -0,0 +1 @@
true 2