- API pages for set and push_back of basic_json_document and for the editable aliases; the class overviews name the Editable parameter - type_error.319 on the exceptions page - the feature page explains editing, and the examples show when it helps: a configuration file changed without reformatting it Signed-off-by: Niels Lohmann <mail@nlohmann.me>
18 KiB
Zero-copy JSON views
#!cpp <nlohmann/json_view.hpp> adds a read-only, non-owning way to look at a parsed JSON text, as an alternative to
building a basic_json tree with parse().
The problem
basic_json::parse() builds a tree of basic_json values: one allocation for every
array and object, and every string copied into its own std::string. That is the right trade-off when the program
goes on to read and write the value freely, but it does more work than necessary when only a small part of a large
JSON text is actually needed, or when the same text is parsed over and over (many small messages, for instance) and
most of the resulting tree is thrown away almost immediately.
The idea
basic_json_document::parse() parses the same JSON grammar, with the same
options, but instead of a tree it builds a flat index of the values it found: one
16-byte entry per value (and one per object key), in document
order. Strings and numbers are not copied out of the input; they stay in the source text, and are only decoded when
actually needed (for a string, only if it contains escape sequences, into one shared buffer owned by the document).
basic_json_view is a small, trivially copyable handle (two pointers) into that
index. It gives you the read-only, type-inspection part of the basic_json interface --
type() and the is_*() predicates,
size()/empty() -- as well as element access
(operator[], at,
front/back), lookup
(find, contains,
count), and iteration
(begin/end,
items) -- without ever allocating a basic_json value. When you do need an
actual basic_json value for a subtree, materialize() builds exactly the
one parse() would have produced for it.
How to use it
Include <nlohmann/json_view.hpp> in addition to (or instead of) <nlohmann/json.hpp>. Parse into a
json_document, inspect its root(), and
materialize() when you need a real value:
??? example "Example: parse a document, inspect its root, and materialize it"
```cpp
--8<-- "examples/json_document.cpp"
```
Output:
```json
--8<-- "examples/json_document.output"
```
ordered_json_document is the equivalent for
ordered_json, just as ordered_json is to
json.
Ownership and lifetime
A document either borrows the text it was parsed from, or owns its own copy of it; call
owns_source() to find out which happened.
parse() decides this from the value category and type of its argument (an
lvalue #!cpp std::string is borrowed; an rvalue #!cpp std::string is moved in, owned without a copy; a stream is
read into an owned buffer; and so on -- see parse's Notes for the full
table). parse_copy() always owns a copy, regardless of the input.
!!! warning "A borrowed document depends on your buffer"
If a document borrows its text, that text **must outlive the document** (and every view taken from it). Reading
or writing through a view after the underlying buffer is gone is undefined behavior, exactly as it would be for
a dangling `#!cpp std::string_view`.
A view is valid only while all of the following hold:
- the document is alive,
- the document has not been re-parsed since the view was taken (with
read()orparse()into it), and has not hadshrink_to_fit()called on it since, and - if the document borrows its source text, that text is still alive.
Moving the document itself is fine and does not invalidate its views: the index is a separate heap allocation
that keeps its address across the move. Take a fresh view from root()
whenever any of the other conditions above was not met.
??? example "Example: borrowed and owned documents, and when views become invalid"
```cpp
--8<-- "examples/json_view_ownership.cpp"
```
Output:
```json
--8<-- "examples/json_view_ownership.output"
```
What is the same as parse()
- 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.
#!cpp allow_exceptions == falsegives 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>.
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 4 GiB or more throws
out_of_range.416, a limit#!cpp basic_json::parse()does not have. - A stream is always read to its end. There is no partial/streaming read of an
#!cpp std::istream. - No source positions on
materialize(). Even withJSON_DIAGNOSTIC_POSITIONSenabled,materialize()does not set them: there is no lexer run during the replay to record them. - Objects iterate in document order.
begin()/end()anditems()visit an object's members in the order they appear in the source text.basic_json's defaultobject_tis astd::map, which sorts by key, so iterating amaterialize()d value can print members in a different order than iterating the view they came from. - Duplicate keys are visible. If an object in the source text repeats a key,
begin()/end()anditems()visit every occurrence (andsize()counts all of them), whileoperator[],at,find,contains, andcountresolve to the first occurrence, since a lookup can stop as soon as it finds a match.basic_json::parse()(and somaterialize()) instead keeps only the last value for a repeated key. See the Notes on duplicate keys ofoperator[]. - No
JSON_DIAGNOSTICSpath. Exceptions thrown bybasic_json_view's own element access and lookup functions never carry the JSON Pointer pathJSON_DIAGNOSTICSwould otherwise add: the view has nobasic_jsonvalue to point at, so the exception is created without one, regardless of howBasicJsonTypewas built. - Ordering comparisons are not provided by
basic_json_view-- there is no#!cpp operator<.operator==andoperator!=are provided, though: two views, or a view and aBasicJsonTypevalue, compare equal exactly whenmaterialize()orparse()would produce equal values for them, without ever building a tree to do it. For ordering, too,materialize()is the way to get a value you can compare.
Getting values out without copying
get<T>() converts many T directly from the flat index, without ever building a
basic_json value for the conversion: #!cpp bool, arithmetic types, #!cpp std::nullptr_t,
#!cpp std::string/other #!cpp std::basic_strings (copied once), basic_json/ordered_json (via
materialize()), basic_json_view itself, #!cpp std::vector<U>, and
#!cpp std::map/#!cpp std::unordered_map with string-like keys. Every other type -- #!cpp std::list,
#!cpp std::pair, #!cpp std::array, enumerations, user types with a from_json() -- goes through
materialize().get<T>() instead: the subtree is built into a real
basic_json value first, exactly as parse() would, and converted from there.
Two conversions never copy at all:
get_string()(equivalently,#!cpp get<string_view_t>()) returns a string as astring_view_tpointing into the document'ssource()text -- or, for a string that contains escape sequences, into the document's own buffer of decoded strings -- instead of allocating a new#!cpp std::string.number_token()returns a number exactly as it was written in the source, e.g.#!cpp "1.50",#!cpp "1E2", or an integer with more digits than any number type holds, instead of rounding it into a#!cpp double/#!cpp int64_tthe way#!cpp get<T>()(andbasic_json::parse()) would.
Both results are only valid as long as the view -- and, for a string with no escapes, the borrowed source text -- is.
Writing a view back
dump() serializes a view directly from the flat index, without ever building a
basic_json value. An object's members are written in document order, not sorted by key, and every occurrence of a
repeated key is written, not only the last one -- the same two ways iteration already differs
from a materialize()d value, see above. #!cpp materialize().dump() gives
a different result in both respects for a json_view.
By default, numbers are written the way basic_json::dump() would.
number_format::source instead copies every number exactly as it was
written in the source text -- a price like #!cpp 19.90, a long order or account ID with more digits than any number
type holds, or a high-precision coordinate -- something basic_json cannot do at all, since parsing already reduces
a number to its parsed #!cpp double/#!cpp int64_t value.
operator<< writes a view to a stream the way basic_json's does, using
the stream's width/fill for indentation.
Editing a document
Everything above is read-only: a json_document/json_view lets you look at a parsed text without copying it, but
not change it. basic_json_document<BasicJsonType, true> -- more conveniently
spelled json_editable_document or
ordered_json_editable_document -- also lets you
set a value and push_back onto
an array, still without ever building a basic_json tree for parts you do not touch.
#!cpp Editable defaults to #!cpp false, so json_document/ordered_json_document are unaffected -- they carry
none of the bookkeeping edits need, and calling set/push_back on one is a compile error, not a runtime one.
Why: editing without reformatting
The #!cpp 19.90 price from above is exactly the kind of value that makes editing a json
or ordered_json value in place lossy. Say you parse a configuration file, patch one field, and write it back:
jsonre-sorts every key on the way in (object_tis a#!cpp std::map) and rewrites every number to its shortest round-trip form on the way out -- a one-field patch turns into a diff that reorders the whole file and rewrites#!cpp 19.90to#!cpp 19.9.ordered_jsonkeeps the key order, but still rewrites every number the same way: parsing has already reduced it to a#!cpp double/#!cpp int64_t, and there is no way back to how it was spelled in the source text.
An editable document keeps both. dump() of an edited document writes members in
document order -- a member set added goes at the end, exactly where it was
inserted -- and number_format::source keeps the exact spelling of
every number an edit did not itself touch; a number an edit did touch is written the way
BasicJsonType::dump() would write it, since there is no source spelling for a brand
new value.
??? example "Example: patch a configuration, keeping member order and number spellings"
```cpp
--8<-- "examples/json_editable_document.cpp"
```
Output:
```json
--8<-- "examples/json_editable_document.output"
```
What stays valid, and what an edit costs
The source text itself is never written, and the parsed index never moves -- a value keeps the node it was
parsed into for as long as it is not itself replaced. So every view taken before
an edit, including a previously obtained root(), stays valid and, if it
still refers to the edited value, sees the edit; a view of a value a later edit drops or replaces just keeps showing
what it last held. New values go to storage the document allocates and owns on demand. The one thing an edit does
invalidate is the iterators taken over an edited array or object: the first time one of its elements is set or
appended to, its elements move from the parsed, fixed layout to a growable block of links so that
push_back can later grow it in amortized constant time -- existing
elements are not touched, but an iterator that was walking the old layout no longer matches. A string obtained with
get_string() is unaffected either way and stays valid across further
edits. See basic_json_document's Edits for the details, and
set's Exception safety for what an edit guarantees if it
throws (the basic guarantee, not the strong one dump() and the read-only functions provide).
Choosing between json, ordered_json, the SAX interface, and json_view
json / ordered_json |
SAX interface | json_document / json_view |
json_editable_document / json_editable_view |
|
|---|---|---|---|---|
| Ownership | owns every value | owns nothing; you decide what to keep, in your handler | borrows or owns the text; the index is always owned by the document | same as json_document; edits go to storage the document owns |
| Mutability | freely mutable | not applicable (a one-shot event stream) | read-only | set/push_back edit in place; the source text is never rewritten |
| What you get | a full tree you can read, write, and keep as long as you like | a sequence of callbacks; whatever your handler builds from them | a flat index plus, on demand, materialize()d json/ordered_json values for the parts you actually use |
the same, plus dump() of an edited document that keeps the member order and, with number_format::source, the spelling of every untouched number |
| Typical use | general-purpose JSON handling: config, request/response bodies you build or modify, anything you hold onto | validating or projecting a text into your own data structure without ever holding the whole thing as JSON | large or high-volume input where you only need part of it, or need it repeatedly, and can keep the source text (or a copy) alive for as long as the document lives | a document you read, patch a few fields of, and write back -- a configuration file, for instance -- where the rest of it should come back exactly as it was |
Version history
- Added in version 3.13.0.