Files
json/docs/mkdocs/docs/features/json_view/differences.md
T
Niels Lohmann f64464daf1 Split the json_view feature page into sub-pages
The single features/json_view.md page grew to ~600 lines covering eight
topics. Split it into features/json_view/ with an overview page and
pages for ownership and lifetime, differences from basic_json, reading
values, generic code, editing, and images.

The new differences page collects what was spread over several sections
("What is the same", "What is different", "Duplicate keys", and notes in
the conversion and serialization sections) under one summary table, and
adds the parts of the basic_json interface a view does not have. The
other pages now link to it instead of restating the rules.

Internal links across the documentation point to the new pages.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 20:42:55 +01:00

15 KiB

Differences from basic_json

A basic_json_view offers the read-only part of the basic_json interface, and in most respects it behaves exactly like the value basic_json::parse() would have built from the same text. This page lists where it does not: what a document parses differently, what a view's functions do differently, and which parts of the basic_json interface a view does not have.

Summary

basic_json value from parse() view of a json_document to get the basic_json behavior
Integer types any number_integer_t/number_unsigned_t 64-bit only (compile-time check) —
Input size unlimited less than 4 GiB —
Parser callbacks parser_callback_t none filter while iterating
operator[] on a missing key or index inserts (non-const) or undefined behavior (const) returns a discarded view —
Object member order sorted by key (json), insertion order (ordered_json) document order materialize()
Repeated keys the last value is kept lookups find the first member; iteration, size(), and dump() see all of them materialize()
Conversions get<T>(), implicit conversions, get_ptr(), get_ref() get<T>(), get_string(), number_token() materialize()
Number spelling lost when parsing kept: number_token(), number_format::source —
Comparison ==, !=, <, <=>, ... == and != only materialize()
Modification member functions of the value only through an editable document materialize()
Iterators bidirectional, reverse iterators forward only materialize()

Each row is one difference; the last column names the way to get `basic_json`'s behavior from a view, where there is one.

Parsing

What is the same

  • Accept/reject. accept() and parse() accept and reject exactly the same inputs as basic_json::accept()/basic_json::parse(), with the same ignore_comments and ignore_trailing_commas options.
  • Errors. A failing parse throws the same exception — the same id, message, and position — because on failure the library's own parser is run on the same bytes to produce the diagnostic. Valid input never pays for this, and the replay builds no values: it costs about a lexing pass up to the error. #!cpp allow_exceptions == false skips the replay and gives a discarded document instead of throwing, just as it gives a discarded value for #!cpp basic_json::parse().
  • Number classification. An integer literal that does not fit into the 64-bit integer type becomes a floating-point value, exactly as it does for #!cpp basic_json::parse().
  • Macros. JSON_STRICT_NUL_HANDLING and JSON_NOEXCEPTION/JSON_THROW_USER behave the same way they do for <nlohmann/json.hpp>. These are the macros that change what the parser accepts or how it reports errors. The other macros only affect the values materialize() produces, through BasicJsonType. Two of them also add context to the exceptions of views, see Exception context.

What is different

  • Only 64-bit integers. basic_json_document<BasicJsonType> requires BasicJsonType::number_integer_t and number_unsigned_t to both be 64 bits wide; this is a compile-time #!cpp static_assert.
  • A 4 GiB input limit. An input of 4294967280 bytes (4 GiB minus 16 bytes) or more throws out_of_range.416, a limit #!cpp basic_json::parse() does not have. It keeps an index entry at 16 bytes: offsets and counts are 32 bits wide. The limit is 16 bytes below 2^32, because the parser reads in 16-byte steps, and a position one step past the end must still fit.
  • A stream is always read to its end. There is no partial/streaming read of an #!cpp std::istream: the stream is read to its end into a buffer the document owns, and parsing starts after that.
  • No parser callbacks. parse() takes no parser_callback_t: a document always indexes the whole text. Skip the values you do not need when reading the view instead.
  • JSON text only. A document is parsed from JSON text; there is no counterpart of from_cbor() and the other binary formats, and is_binary() is always #!cpp false.

Element access

Chained access is safe. operator[] with a missing key, an index out of range, or an unresolvable JSON pointer returns a discarded view, and operator[] on a discarded view returns a discarded view without throwing: #!cpp v["a"]["b"][0] can be tested once at the end. Type errors on values that exist (a key on an array, an index on an object) still throw, and at throws for every missing value.

basic_json's non-const operator[] instead inserts a missing key, and its const operator[] has undefined behavior for one.

Objects

Object member order

begin()/end() and items() visit an object's members in the order they appear in the source text. basic_json's default object_t is a std::map, which sorts by key, so iterating a materialize()d value can print members in a different order than iterating the view they came from. dump() of a view writes members in document order as well.

Duplicate keys

!!! warning "A lookup in a view and in the parsed value can give different answers"

If an object in the source text repeats a key, [`operator[]`](../../api/basic_json_view/operator%5B%5D.md),
[`at`](../../api/basic_json_view/at.md), [`find`](../../api/basic_json_view/find.md),
[`value`](../../api/basic_json_view/value.md), [`contains`](../../api/basic_json_view/contains.md), and JSON
pointer resolution all return the **first** member with that key. `basic_json::parse()` instead keeps the **last**
value of a repeated key, so for `#!cpp {"a":1,"a":2}`, `#!cpp view["a"]` is `#!cpp 1` while
`#!cpp parse(text)["a"]` is `#!cpp 2`. [`count`](../../api/basic_json_view/count.md) counts every member with the
key, so it is `#!cpp 2` here.

begin()/end() and items() visit every occurrence, in document order, and size() counts all of them, so #!cpp view.size() can be larger than #!cpp view.materialize().size(). dump() writes every occurrence, too, so #!cpp materialize().dump() gives a different result for such an object.

Why the first? A lookup can stop as soon as it finds a match. Returning the last member would force every lookup to scan all members of the object, even when the key is found at the very first one: this made lookups in small objects 1.6 to 3.4 times slower. The hash index of the keys of objects with 128 members or more leads to the first member of a key as well. Other zero-copy parsers that index the source text, such as yyjson and simdjson, also return the first member. RFC 8259 only says that names within an object SHOULD be unique and that the behavior of a receiver that sees duplicates is unpredictable, so neither choice is wrong.

The same holds for get<T>() of a type whose from_json() reads the view, such as the types of the NLOHMANN_DEFINE_TYPE_* macros: their members are looked up, so #!cpp get<point>() of #!cpp {"x":1,"x":2} reads #!cpp x == 1, while #!cpp materialize().get<point>() reads #!cpp 2.

What stays the same as parse()? materialize() and get<std::map<...>>() (and the other containers built from an object) replay every member in order, so they keep the last value exactly like #!cpp basic_json::parse() (at the position of the first occurrence of the key, for an ordered_json).

How do I get the value parse() would give? Either call #!cpp view.materialize() and look the key up in the result, or iterate the members and keep the last match:

// the last member with the key "a", as parse() would keep it
json_view last;
for (const auto item : view.items())
{
    if (item.key() == "a")
    {
        last = item.value();
    }
}

If you cannot trust the source text to have unique keys, check size() against the number of distinct keys, or reject duplicates when iterating.

Conversions

  • No implicit conversions. A view has no #!cpp operator ValueType(), no #!cpp get_ptr(), and no #!cpp get_ref(); use get<T>(), or get_string() for a string without a copy.
  • from_json() overloads. get<T>() calls T's from_json() with the view only if that from_json() accepts views, and with materialize() otherwise; see A from_json() that reads views. Both give the same result except for repeated keys.
  • string_view members. A struct with a #!cpp std::string_view member decoded from a view points into the document, see string_view members. Decoding the same struct from a json value compiles, too, but dangles at once if the value is a temporary.

Serialization

dump() writes numbers the way basic_json::dump() would by default, but writes object members in document order and writes every occurrence of a repeated key.

A view can also do what a basic_json value cannot: number_token() and number_format::source give every number exactly as it was spelled in the source text, while parsing into basic_json already reduces a number to its #!cpp double/#!cpp int64_t value. See Writing a view back.

Comparison

Ordering comparisons are not provided by basic_json_view — there is no #!cpp operator<. operator== and operator!= are provided, though: two views, or a view and a BasicJsonType value, compare equal exactly when materialize() or parse() would produce equal values for them, without ever building a tree to do it. For ordering, materialize() is the way to get a value you can compare.

Exception context

With JSON_DIAGNOSTICS, an exception of a view names the JSON Pointer path of the value within the document, and with JSON_DIAGNOSTIC_POSITIONS, it names the bytes of the value in the source text, as they do for the exceptions of #!cpp basic_json.

A view has neither parent pointers nor stored positions, so both are found from the document's index when the exception is thrown. This takes time linear in the size of the document. One exception has neither: key() of an iterator over an array or a primitive value (invalid_iterator.207), because a view's iterator does not know the value it iterates over.

Fewer source positions after edits. With JSON_DIAGNOSTIC_POSITIONS enabled, materialize() sets the same positions as #!cpp basic_json::parse(). In an editable document, a value written by an edit has none, and neither has an array or object whose elements changed or whose last element has none; a basic_json value keeps its positions when it is changed. See materialize().

What a view does not have

A view is a read-only handle. These parts of the basic_json interface have no counterpart on a view; materialize() the subtree to use them:

See also

Version history

  • Added in version 3.13.0.