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>
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()andparse()accept and reject exactly the same inputs asbasic_json::accept()/basic_json::parse(), with the sameignore_commentsandignore_trailing_commasoptions. - 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 == falseskips 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_HANDLINGandJSON_NOEXCEPTION/JSON_THROW_USERbehave 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 valuesmaterialize()produces, throughBasicJsonType. 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>requiresBasicJsonType::number_integer_tandnumber_unsigned_tto 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 noparser_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, andis_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(); useget<T>(), orget_string()for a string without a copy. from_json()overloads.get<T>()callsT'sfrom_json()with the view only if thatfrom_json()accepts views, and withmaterialize()otherwise; see Afrom_json()that reads views. Both give the same result except for repeated keys.string_viewmembers. A struct with a#!cpp std::string_viewmember decoded from a view points into the document, seestring_viewmembers. Decoding the same struct from ajsonvalue 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:
- Modifiers such as
push_back,insert,erase,update, oroperator=. A view never changes a value itself; an editable document changes the values its views refer to throughset,push_back,insert, anderase. - Reverse iteration. A view's iterator is a forward iterator; there is no
rbegin()/rend(). - Ordering comparisons, see Comparison.
- JSON Patch and friends:
patch(),merge_patch(),diff(),flatten(). - Binary formats:
to_cbor()and the other writers, and binary values (get_binary()).
See also
- Zero-copy JSON views - overview
basic_json_view- the full interface of a viewmaterialize()- build thebasic_jsonvalue of a view
Version history
- Added in version 3.13.0.