From 7b755737b453f8962d2e0e7be0bb3a5690f1a409 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sun, 18 Oct 2026 10:01:16 +0200 Subject: [PATCH] Address the review of json_view element access From gregmarr's review of #5622: - count() counts every member with the key, so count(key) > 1 detects duplicate keys; it was contains() as 0/1 before. - get_to() passes types that get() does not convert directly to from_json with the existing value, as basic_json::get_to() does, so that a from_json which sets only the fields present keeps the others. - array_index() checks the empty token and "-" first; the results are unchanged. - Docs: no "if an exception is thrown" where a view never changes anything; the borrowed source must also stay unmodified; items() on a temporary view is safe; back()/front() notes shortened; the basic_json see-also entries say how the view's results can differ. Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/find.md | 3 +- docs/mkdocs/docs/api/basic_json/front.md | 3 +- docs/mkdocs/docs/api/basic_json/items.md | 2 +- docs/mkdocs/docs/api/basic_json/operator[].md | 5 ++- docs/mkdocs/docs/api/basic_json_view/at.md | 2 +- docs/mkdocs/docs/api/basic_json_view/back.md | 5 +-- docs/mkdocs/docs/api/basic_json_view/begin.md | 6 ++- .../docs/api/basic_json_view/contains.md | 3 +- docs/mkdocs/docs/api/basic_json_view/count.md | 35 ++++++++---------- docs/mkdocs/docs/api/basic_json_view/front.md | 6 +-- docs/mkdocs/docs/api/basic_json_view/get.md | 17 +++++---- .../docs/api/basic_json_view/get_string.md | 9 ++++- .../mkdocs/docs/api/basic_json_view/get_to.md | 31 ++++++++++++---- docs/mkdocs/docs/api/basic_json_view/index.md | 23 ++++++++---- docs/mkdocs/docs/api/basic_json_view/items.md | 18 ++++----- .../docs/api/basic_json_view/number_token.md | 2 +- .../docs/api/basic_json_view/operator[].md | 4 +- .../docs/api/basic_json_view/type_name.md | 16 ++++---- docs/mkdocs/docs/api/basic_json_view/value.md | 2 +- .../docs/examples/basic_json_view__count.cpp | 12 +++--- .../examples/basic_json_view__count.output | 3 +- docs/mkdocs/docs/features/json_view.md | 17 +++++---- include/nlohmann/detail/view/lookup.hpp | 15 ++++++++ include/nlohmann/detail/view/pointer.hpp | 24 ++++++------ include/nlohmann/detail/view/value.hpp | 22 +++++++++++ include/nlohmann/json_view.hpp | 31 ++++++++++++---- tests/src/unit-json_view.cpp | 37 ++++++++++++++++++- 27 files changed, 239 insertions(+), 114 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/find.md b/docs/mkdocs/docs/api/basic_json/find.md index d8715a61a..92ab380c1 100644 --- a/docs/mkdocs/docs/api/basic_json/find.md +++ b/docs/mkdocs/docs/api/basic_json/find.md @@ -93,7 +93,8 @@ Logarithmic in the size of the JSON object. - [count](count.md) returns the number of occurrences of a key - [contains](contains.md) checks whether a key exists -- [basic_json_view::find](../basic_json_view/find.md) - the same lookup on a zero-copy view +- [basic_json_view::find](../basic_json_view/find.md) - the same lookup on a zero-copy view (a duplicate key resolves + to the first member, whereas `parse()` keeps the last) ## Version history diff --git a/docs/mkdocs/docs/api/basic_json/front.md b/docs/mkdocs/docs/api/basic_json/front.md index ef4f0d262..dbe92f589 100644 --- a/docs/mkdocs/docs/api/basic_json/front.md +++ b/docs/mkdocs/docs/api/basic_json/front.md @@ -51,7 +51,8 @@ Constant. ## See also - [back](back.md) to access the last element -- [basic_json_view::front](../basic_json_view/front.md) - the same access on a zero-copy view +- [basic_json_view::front](../basic_json_view/front.md) - the same access on a zero-copy view (document order, not + sorted; duplicate keys are visited) ## Version history diff --git a/docs/mkdocs/docs/api/basic_json/items.md b/docs/mkdocs/docs/api/basic_json/items.md index 5fb8a7bf1..06e725ddd 100644 --- a/docs/mkdocs/docs/api/basic_json/items.md +++ b/docs/mkdocs/docs/api/basic_json/items.md @@ -100,7 +100,7 @@ When iterating over an array, `key()` will return the index of the element as st - [begin](begin.md) returns an iterator to the first element - [end](end.md) returns an iterator to one past the last element - [basic_json_view::items](../basic_json_view/items.md) - the same range on a zero-copy view (`#!cpp const auto`, - not `#!cpp const auto&`: items are produced on the fly) + not `#!cpp const auto&`: items are produced on the fly; document order, not sorted; duplicate keys are visited) ## Version history diff --git a/docs/mkdocs/docs/api/basic_json/operator[].md b/docs/mkdocs/docs/api/basic_json/operator[].md index b88ead33d..d8df9b73a 100644 --- a/docs/mkdocs/docs/api/basic_json/operator[].md +++ b/docs/mkdocs/docs/api/basic_json/operator[].md @@ -275,8 +275,9 @@ Strong exception safety: if an exception occurs, the original value stays intact - documentation on [runtime assertions](../../features/assertions.md) - see [`at`](at.md) for access by reference with range checking - see [`value`](value.md) for access with default value -- [basic_json_view::operator[]](../basic_json_view/operator%5B%5D.md) - the same access on a zero-copy view (always - returns a discarded view instead of assuming undefined behavior) +- [basic_json_view::operator[]](../basic_json_view/operator%5B%5D.md) - the same access on a zero-copy view (a duplicate + key resolves to the first member, whereas `parse()` keeps the last; a missing key gives a discarded view instead of + undefined behavior) ## Version history diff --git a/docs/mkdocs/docs/api/basic_json_view/at.md b/docs/mkdocs/docs/api/basic_json_view/at.md index a0a57c9bc..8d6fed441 100644 --- a/docs/mkdocs/docs/api/basic_json_view/at.md +++ b/docs/mkdocs/docs/api/basic_json_view/at.md @@ -41,7 +41,7 @@ basic_json_view at(const json_pointer& ptr) const; ## Exception safety -Strong exception safety: if an exception is thrown, there are no changes to the view or the document it refers to. +Strong guarantee: there are no changes to the view or the document it refers to. ## Exceptions diff --git a/docs/mkdocs/docs/api/basic_json_view/back.md b/docs/mkdocs/docs/api/basic_json_view/back.md index 5ddb1fb35..1d70e9feb 100644 --- a/docs/mkdocs/docs/api/basic_json_view/back.md +++ b/docs/mkdocs/docs/api/basic_json_view/back.md @@ -13,7 +13,7 @@ The last element or member value. For a primitive value (number, string, boolean ## Exception safety -Strong exception safety: if an exception is thrown, there are no changes to the view or the document it refers to. +Strong guarantee: there are no changes to the view or the document it refers to. ## Exceptions @@ -33,8 +33,7 @@ index. ## Notes Unlike [`BasicJsonType::back()`](../basic_json/back.md), which has undefined behavior for an empty array or object, -`back()` throws `invalid_iterator.214` in that case -- the same way it already does for `#!json null` and for a -discarded view, where `BasicJsonType::back()` also throws. +`back()` throws `invalid_iterator.214` for an empty array, an empty object, a null value, or a discarded view. ## Examples diff --git a/docs/mkdocs/docs/api/basic_json_view/begin.md b/docs/mkdocs/docs/api/basic_json_view/begin.md index ae2665ee9..afc662ee7 100644 --- a/docs/mkdocs/docs/api/basic_json_view/begin.md +++ b/docs/mkdocs/docs/api/basic_json_view/begin.md @@ -23,7 +23,7 @@ Constant. ## Notes For an object, iteration visits **every** member, including all occurrences of a duplicate key -- unlike -[`operator[]`](operator[].md), [`at`](at.md), [`find`](find.md), [`contains`](contains.md), and [`count`](count.md), +[`operator[]`](operator[].md), [`at`](at.md), [`find`](find.md), and [`contains`](contains.md), which all resolve to the *first* member with a given key. See the [Notes on duplicate keys](operator[].md#notes) of `operator[]`. @@ -31,6 +31,10 @@ Because objects are iterated in document order rather than sorted by key, the or iterating the [`materialize()`](materialize.md)d `BasicJsonType` value would produce: a `#!cpp basic_json` object (`std::map`-backed by default) sorts its keys, while a view does not. +An `ordered_json` value also keeps document order, but it keeps only one entry per key: as `parse()` does, it keeps the +*last* value, at the position of the key's *first* occurrence. The view, in contrast, visits every member, including +the duplicates. + ## Examples ??? example diff --git a/docs/mkdocs/docs/api/basic_json_view/contains.md b/docs/mkdocs/docs/api/basic_json_view/contains.md index 5094fe25c..ab9596d3d 100644 --- a/docs/mkdocs/docs/api/basic_json_view/contains.md +++ b/docs/mkdocs/docs/api/basic_json_view/contains.md @@ -53,7 +53,8 @@ view. !!! info "Postconditions" - If `#!cpp v.contains(key)` returns `#!cpp true`, then `#!cpp v[key]` is not [discarded](is_discarded.md). If + If `#!cpp v.contains(key)` returns `#!cpp true`, then `#!cpp v[key]` is not [discarded](is_discarded.md) and + `#!cpp v.at(key)` does not throw. If `#!cpp v.contains(ptr)` returns `#!cpp true`, then `#!cpp v[ptr]` is not discarded and `#!cpp v.at(ptr)` does not throw. diff --git a/docs/mkdocs/docs/api/basic_json_view/count.md b/docs/mkdocs/docs/api/basic_json_view/count.md index 30d90b8e1..698c7e0be 100644 --- a/docs/mkdocs/docs/api/basic_json_view/count.md +++ b/docs/mkdocs/docs/api/basic_json_view/count.md @@ -6,7 +6,8 @@ size_type count(const char* key) const; size_type count(const string_t& key) const; ``` -Returns `#!cpp 1` if the value is an object with a member with key `key`, `#!cpp 0` otherwise. +Returns the number of members with key `key` if the value is an object, `#!cpp 0` otherwise. Members with a repeated +key are all counted. ## Parameters @@ -15,7 +16,8 @@ Returns `#!cpp 1` if the value is an object with a member with key `key`, `#!cpp ## Return value -`#!cpp 1` if the value is an object and has a member with key `key`, `#!cpp 0` otherwise. +the number of members of the object with key `key` (duplicates included), or `#!cpp 0` if the value is not an object +or has no such member. ## Exception safety @@ -23,34 +25,29 @@ No-throw guarantee: this function never throws exceptions. ## Complexity -Linear in the number of members: as for [`ordered_json`](../ordered_json.md), members are compared one after another, -in document order, stopping at the first match. Each comparison first checks the key's length --- already known from the index, without reading the key bytes -- before comparing its content. +Linear in the number of members: unlike the lookup functions, `count()` does not stop at the first match, because it +has to count every member with the key. Members are compared one after another, in document order. Each comparison +first checks the key's length -- already known from the index, without reading the key bytes -- before comparing its +content. ## Notes -!!! warning "Duplicate keys: the first member wins" - - If the source text repeats a key, this resolves to the *first* member with it, not to the last one that - [`materialize()`](materialize.md) and `parse()` keep. See the [Notes on duplicate keys](operator[].md#notes) of - `operator[]` and [Duplicate keys](../../features/json_view.md#duplicate-keys) for the reasons and for how to get - the last value. - This method always returns `#!cpp 0` when the value is not an object -- including a [discarded](is_discarded.md) view. -Unlike [`BasicJsonType::count()`](../basic_json/count.md), whose return value can in principle exceed `#!cpp 1` for -an `ObjectType` that allows multiple entries per key, `count()` here never does: it is exactly -[`contains()`](contains.md) as `#!cpp 0`/`#!cpp 1`. This holds even if the source text has a duplicate key -- see the -[Notes on duplicate keys](operator[].md#notes) of `operator[]` -- because a `#!cpp count() > 1` result would require -counting every member with a matching key (the lookup functions resolve to the *first* one). +If the source text repeats a key, `count()` returns more than `#!cpp 1` for it, even though the +[materialized](materialize.md) value (like the result of `parse()`) would have only one member with that key. Hence +`#!cpp count(key) > 1` detects duplicate keys. This is unlike the lookup functions +([`operator[]`](operator[].md), [`at`](at.md), [`find`](find.md), [`contains`](contains.md)), which resolve to the +*first* member; see the [Notes on duplicate keys](operator[].md#notes) of `operator[]`. ## Examples ??? example - The example below validates that every transaction of a batch carries a mandatory `amount` field, using - `count()` before deciding whether to materialize a transaction at all. + The example below validates that every transaction of a batch carries exactly one `amount` field, using + `count()` before deciding whether to materialize a transaction at all. It reports a transaction with no `amount` + field and one with a duplicated `amount` field. ```cpp --8<-- "examples/basic_json_view__count.cpp" diff --git a/docs/mkdocs/docs/api/basic_json_view/front.md b/docs/mkdocs/docs/api/basic_json_view/front.md index c96a35eaf..e47f5d01e 100644 --- a/docs/mkdocs/docs/api/basic_json_view/front.md +++ b/docs/mkdocs/docs/api/basic_json_view/front.md @@ -13,7 +13,7 @@ The first element or member value. For a primitive value (number, string, boolea ## Exception safety -Strong exception safety: if an exception is thrown, there are no changes to the view or the document it refers to. +Strong guarantee: there are no changes to the view or the document it refers to. ## Exceptions @@ -31,8 +31,8 @@ Constant. ## Notes Unlike [`BasicJsonType::front()`](../basic_json/front.md), which has undefined behavior for an empty array or -object, `front()` throws `invalid_iterator.214` in that case -- the same way it already does for `#!json null` and -for a discarded view, where `BasicJsonType::front()` also throws. +object, `front()` throws `invalid_iterator.214` for an empty array, an empty object, a null value, or a discarded +view. ## Examples diff --git a/docs/mkdocs/docs/api/basic_json_view/get.md b/docs/mkdocs/docs/api/basic_json_view/get.md index d8ec333c9..7fe30ca7f 100644 --- a/docs/mkdocs/docs/api/basic_json_view/get.md +++ b/docs/mkdocs/docs/api/basic_json_view/get.md @@ -10,8 +10,8 @@ Converts the value to `T`. For the types below, the conversion works directly on the flat index -- no `BasicJsonType` value is built for it: - `#!cpp bool` -- arithmetic types other than `#!cpp bool` (from a number; from a boolean, as `#!cpp 0`/`#!cpp 1`, exactly as - [`BasicJsonType::get()`](../basic_json/get.md) converts a boolean) +- arithmetic types other than `#!cpp bool` (for a boolean, `#!cpp 0`/`#!cpp 1`, exactly as for + [`BasicJsonType::get()`](../basic_json/get.md)) - `#!cpp std::nullptr_t` - `#!cpp std::basic_string` (including `string_t`) -- a copy of the string - [`string_view_t`](index.md#member-types) -- **no copy**: the returned view points into the document's @@ -19,10 +19,10 @@ For the types below, the conversion works directly on the flat index -- no `Basi document's own buffer of decoded strings (see [`get_string()`](get_string.md)) - `BasicJsonType` -- equivalent to [`materialize()`](materialize.md) - `basic_json_view` -- returns `#!cpp *this` -- `#!cpp std::vector` -- element by element, each converted with `#!cpp get()`; `#!cpp +- `#!cpp std::vector` -- from an array, element by element, each converted with `#!cpp get()`; `#!cpp std::vector` keeps a view of every element instead of a value - `#!cpp std::map` and `#!cpp std::unordered_map`, if `K` is constructible from a `#!cpp - (const char*, std::size_t)` pair -- member by member, each value converted with `#!cpp get()`; with a repeated + (const char*, std::size_t)` pair -- from an object, member by member, each value converted with `#!cpp get()`; with a repeated key, the *last* value is kept, as [`BasicJsonType::parse()`](../basic_json/parse.md) (and [`materialize()`](materialize.md)) does; `#!cpp std::map` keeps views of the members instead of values @@ -43,7 +43,7 @@ the value, converted to `T` ## Exception safety -Strong exception safety: if an exception is thrown, there are no changes to the view or the document it refers to. +Strong guarantee: there are no changes to the view or the document it refers to. ## Exceptions @@ -62,6 +62,9 @@ None of the exceptions thrown directly by this function (the first three bullets [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no `BasicJsonType` value to point at. An exception thrown while converting through `materialize()` (the last bullet) is different: it is thrown by a real `BasicJsonType` value, so it **does** carry a `JSON_DIAGNOSTICS` path if `BasicJsonType` was built with it enabled. +That path is relative to the root of the view's subtree, because `materialize()` builds only the subtree (with parent +pointers): it is empty if the failing value is the subtree's root itself, and it never is the path within the whole +document. ## Complexity @@ -73,8 +76,8 @@ exception thrown while converting through `materialize()` (the last bullet) is d - `#!cpp std::map`/`#!cpp std::unordered_map`: linear in the number of members for walking them, plus the container's own insertion cost per member (logarithmic for `#!cpp std::map`, amortized constant for `#!cpp std::unordered_map`), times the complexity of converting one member to the mapped type. -- every other `T`: linear in the size of the subtree (building the `BasicJsonType` value), plus the complexity of - [`BasicJsonType::get()`](../basic_json/get.md) on it. +- every other `T`: the cost of [`materialize()`](materialize.md) and of destroying the `BasicJsonType` value + afterwards, plus that of [`BasicJsonType::get()`](../basic_json/get.md). ## Notes diff --git a/docs/mkdocs/docs/api/basic_json_view/get_string.md b/docs/mkdocs/docs/api/basic_json_view/get_string.md index 1222e3bd6..7c9aec2a4 100644 --- a/docs/mkdocs/docs/api/basic_json_view/get_string.md +++ b/docs/mkdocs/docs/api/basic_json_view/get_string.md @@ -15,7 +15,7 @@ had to be decoded once when the document was parsed). ## Exception safety -Strong exception safety: if an exception is thrown, there are no changes to the view or the document it refers to. +Strong guarantee: there are no changes to the view or the document it refers to. ## Exceptions @@ -37,9 +37,14 @@ hand out a reference to a stored `string_t`. `get_string()` (equivalently, [`get zero-copy alternative: [`BasicJsonType::get_ref()`](../basic_json/get_ref.md) is its closest counterpart, except that it returns a view instead of a reference to a value that must already exist. +`get_string()` borrows the string: nothing is copied, whereas [`get()`](get.md) copies it. For a borrowed +document, the returned [`string_view_t`](index.md#member-types) is valid as long as the caller's buffer; for an owned +one, as long as the document. + The returned [`string_view_t`](index.md#member-types) is valid exactly as long as the view that produced it -- see the [validity rules](index.md) of `basic_json_view` -- and, for a string with no escapes, for as long as the document's -source text. +source text, which for a borrowed document must remain valid **and unmodified** for as long as the document exists, +since floating-point values and unescaped strings are read from the source lazily. ## Examples diff --git a/docs/mkdocs/docs/api/basic_json_view/get_to.md b/docs/mkdocs/docs/api/basic_json_view/get_to.md index 6eabe7899..4f1c6a5d9 100644 --- a/docs/mkdocs/docs/api/basic_json_view/get_to.md +++ b/docs/mkdocs/docs/api/basic_json_view/get_to.md @@ -5,12 +5,15 @@ template T& get_to(T& v) const; ``` -Converts the value to `T` and assigns it to `v`. Equivalent to +Converts the value to `T` and stores it in `v`, in one of two ways: -```cpp -v = get(); -return v; -``` +- For the types that [`get()`](get.md) converts directly -- arithmetic types, `#!cpp bool`, + `#!cpp std::nullptr_t`, [`string_view_t`](index.md#member-types), `#!cpp std::basic_string`, `BasicJsonType`, + `basic_json_view`, `#!cpp std::vector`, and `#!cpp std::map`/`#!cpp std::unordered_map` with string keys -- `v` is + assigned: `#!cpp v = get()`. +- For every other type, the subtree is built into a `BasicJsonType` value and `#!cpp materialize().get_to(v)` is + called, i.e. `from_json()` is invoked with the existing `v`, exactly as + [`BasicJsonType::get_to()`](../basic_json/get_to.md) does. ## Template parameters @@ -28,15 +31,27 @@ return v; ## Exception safety -Strong exception safety: if an exception is thrown, `v` is not modified. +- For the directly converted types: strong exception safety. If an exception is thrown, `v` is not modified. +- For every other type: as for [`BasicJsonType::get_to()`](../basic_json/get_to.md). `materialize()` throws before `v` + is touched, but a `from_json()` that throws may leave `v` partly modified. ## Exceptions -Whatever [`get()`](get.md) throws for the same value and `T`. +- For the directly converted types: whatever [`get()`](get.md) throws for the same value and `T`. +- For every other type: whatever [`materialize()`](materialize.md) and + [`BasicJsonType::get_to()`](../basic_json/get_to.md) throw. ## Complexity -Whatever [`get()`](get.md) has for the same `T`. +- For the directly converted types: whatever [`get()`](get.md) has for the same `T`. +- For every other type: that of [`materialize()`](materialize.md), plus that of + [`BasicJsonType::get_to()`](../basic_json/get_to.md). + +## Notes + +The difference between the two ways matters for a `from_json()` that sets only the fields present in the JSON: since +it runs on the existing `v`, the other fields of `v` keep their values, whereas assigning `get()` would replace +them with those of a default-constructed `T`. ## Examples diff --git a/docs/mkdocs/docs/api/basic_json_view/index.md b/docs/mkdocs/docs/api/basic_json_view/index.md index 6123cf958..d0fdb6fc5 100644 --- a/docs/mkdocs/docs/api/basic_json_view/index.md +++ b/docs/mkdocs/docs/api/basic_json_view/index.md @@ -14,17 +14,25 @@ to the document and a pointer into its index), trivially copyable. A view is val - the document has not been re-parsed with [`read()`](../basic_json_document/read.md) (or [`parse()`](../basic_json_document/parse.md) into it) or shrunk with [`shrink_to_fit()`](../basic_json_document/shrink_to_fit.md) since the view was taken, and -- if the document borrows its source text, that text is still alive. +- if the document borrows its source text, that text is still alive and unmodified (floating-point values and + unescaped strings are read from it lazily). Moving the document itself does not invalidate its views: the index is heap-allocated independently of the `basic_json_document` object. -`basic_json_view` provides the read-only part of the `BasicJsonType` interface: the type-inspection functions, element -access, lookup, iteration, and conversion -- [`get()`](get.md), [`get_string()`](get_string.md), -[`number_token()`](number_token.md), and [`materialize()`](materialize.md) to build the `BasicJsonType` value of a -subtree on demand. [`operator[]`](operator%5B%5D.md), [`at`](at.md), [`contains`](contains.md), and -[`value`](value.md) also accept a [`json_pointer`](../json_pointer/index.md). It does not (yet) provide `dump()` or -comparison. +`basic_json_view` provides the read-only part of the `BasicJsonType` interface: + +- the type-inspection functions, +- element access, +- lookup, +- iteration, and +- conversion: [`get()`](get.md), [`get_string()`](get_string.md), [`number_token()`](number_token.md), and + [`materialize()`](materialize.md) to build the `BasicJsonType` value of a subtree on demand. + +[`operator[]`](operator%5B%5D.md), [`at`](at.md), [`contains`](contains.md), and [`value`](value.md) also accept a +[`json_pointer`](../json_pointer/index.md). It does not (yet) provide `dump()` or comparison. Unlike `basic_json`, +there are no implicit conversion operators (no `#!cpp operator ValueType()`), and no `#!cpp get_ptr()` or +`#!cpp get_ref()`; see [`get`](get.md). ## Template parameters @@ -46,7 +54,6 @@ comparison. - **string_view_t** - `#!cpp std::string_view` on C++17 and newer, a minimal internal substitute otherwise - **iterator**, **const_iterator** - a forward iterator over the elements of an array or the member values of an object, in document order; both names refer to the same type, since a view is always read-only -- **item** - a (key, value) pair produced by [`items()`](items.md) ## Member functions diff --git a/docs/mkdocs/docs/api/basic_json_view/items.md b/docs/mkdocs/docs/api/basic_json_view/items.md index d54f8d723..1e2a1b802 100644 --- a/docs/mkdocs/docs/api/basic_json_view/items.md +++ b/docs/mkdocs/docs/api/basic_json_view/items.md @@ -4,9 +4,9 @@ /* unspecified */ items() const noexcept; ``` -Returns a range of [`item`](index.md#member-types) values -- (key, value) pairs -- for use in range-based for loops. -The key of an array element is its index, converted to a string, as for -[`BasicJsonType::items()`](../basic_json/items.md). +Returns a range of items -- (key, value) pairs -- for use in range-based for loops. An item's `#!cpp key()` is a +`#!cpp string_view_t` (for an array element, its index converted to a string, as for +[`BasicJsonType::items()`](../basic_json/items.md)), and its `#!cpp value()` is a `basic_json_view` of the element. The returned type is not part of the public API and may change between versions; use a range-based for loop (see the example), or `#!cpp decltype(v.items())` if you need to name it. @@ -33,8 +33,8 @@ is nothing for a reference to bind to. ## Return value -A range whose iterators dereference to [`item`](index.md#member-types) and whose `#!cpp begin()`/`#!cpp end()` are -equivalent to [`basic_json_view::begin()`](begin.md)/[`end()`](end.md), in document order. +A range whose iterators dereference to an item (with `#!cpp key()` and `#!cpp value()`, see above) and whose +`#!cpp begin()`/`#!cpp end()` are equivalent to [`basic_json_view::begin()`](begin.md)/[`end()`](end.md), in document order. ## Exception safety @@ -48,14 +48,14 @@ Constant. As for [`begin()`](begin.md)/[`end()`](end.md), `items()` visits **every** member of an object, including all occurrences of a duplicate key -- unlike [`operator[]`](operator[].md), [`at`](at.md), [`find`](find.md), -[`contains`](contains.md), and [`count`](count.md), which resolve to the *first* member with a given key. See the +and [`contains`](contains.md), which resolve to the *first* member with a given key. See the [Notes on duplicate keys](operator[].md#notes) of `operator[]`. !!! danger "Lifetime issues" - As for `BasicJsonType::items()`, calling `items()` on a temporary view (or a temporary document) is dangerous: - the range refers back to the document, so the document must outlive the loop. See - [#2040](https://github.com/nlohmann/json/issues/2040) for the `BasicJsonType` background. + Unlike `BasicJsonType::items()` (see [#2040](https://github.com/nlohmann/json/issues/2040)), calling `items()` on + a temporary view is safe, for instance `#!cpp for (auto item : v["a"].items())`: the returned range holds a copy + of the view. What must outlive the loop is the document and, for a borrowed document, the source buffer. ## Examples diff --git a/docs/mkdocs/docs/api/basic_json_view/number_token.md b/docs/mkdocs/docs/api/basic_json_view/number_token.md index 25a82992a..d7c56c22b 100644 --- a/docs/mkdocs/docs/api/basic_json_view/number_token.md +++ b/docs/mkdocs/docs/api/basic_json_view/number_token.md @@ -15,7 +15,7 @@ and [`materialize()`](materialize.md) can only represent approximately, as a `nu ## Exception safety -Strong exception safety: if an exception is thrown, there are no changes to the view or the document it refers to. +Strong guarantee: there are no changes to the view or the document it refers to. ## Exceptions diff --git a/docs/mkdocs/docs/api/basic_json_view/operator[].md b/docs/mkdocs/docs/api/basic_json_view/operator[].md index 00ab1e9ac..b2f3097a9 100644 --- a/docs/mkdocs/docs/api/basic_json_view/operator[].md +++ b/docs/mkdocs/docs/api/basic_json_view/operator[].md @@ -50,7 +50,7 @@ basic_json_view operator[](const json_pointer& ptr) const; ## Exception safety -Strong exception safety: if an exception is thrown, there are no changes to the view or the document it refers to. +Strong guarantee: there are no changes to the view or the document it refers to. ## Exceptions @@ -108,7 +108,7 @@ document. !!! warning "Duplicate keys: the first member wins" If the source text has an object with a duplicate key, `#!cpp operator[]` (and [`at`](at.md), [`find`](find.md), - [`contains`](contains.md), [`count`](count.md), [`value`](value.md), and JSON pointer resolution) all resolve to + [`contains`](contains.md), [`value`](value.md), and JSON pointer resolution) all resolve to the *first* member with that key, because a lookup can stop as soon as it finds a match. This is different from [`materialize()`](materialize.md) (and [`BasicJsonType::parse()`](../basic_json/parse.md)), which replay every member in order and so keep the *last* value for a repeated key, so `#!cpp v["a"]` and diff --git a/docs/mkdocs/docs/api/basic_json_view/type_name.md b/docs/mkdocs/docs/api/basic_json_view/type_name.md index b5bde001d..d30132886 100644 --- a/docs/mkdocs/docs/api/basic_json_view/type_name.md +++ b/docs/mkdocs/docs/api/basic_json_view/type_name.md @@ -14,14 +14,14 @@ same string for a discarded `BasicJsonType` value). a string representation of the type ([`value_t`](../basic_json/value_t.md)): | Value type | return value | -|-----------------------------------------------------|---------------| -| `#!json null` | `"null"` | -| boolean | `"boolean"` | -| string | `"string"` | -| number (integer, unsigned integer, floating-point) | `"number"` | -| object | `"object"` | -| array | `"array"` | -| discarded | `"discarded"` | +|----------------------------------------------------|---------------| +| `#!json null` | `"null"` | +| boolean | `"boolean"` | +| string | `"string"` | +| number (integer, unsigned integer, floating-point) | `"number"` | +| object | `"object"` | +| array | `"array"` | +| discarded | `"discarded"` | `type_name()` never returns `#!cpp "binary"`, since a JSON text has no binary values (see [`is_binary()`](is_binary.md)); it also never returns `#!cpp "invalid"`, since a view's `#!cpp kind` always comes diff --git a/docs/mkdocs/docs/api/basic_json_view/value.md b/docs/mkdocs/docs/api/basic_json_view/value.md index 2a8b9d828..2a9eaedb9 100644 --- a/docs/mkdocs/docs/api/basic_json_view/value.md +++ b/docs/mkdocs/docs/api/basic_json_view/value.md @@ -44,7 +44,7 @@ equivalent) deduce `string_t`, not `const char*`, for their return type and for ## Exception safety -Strong exception safety: if an exception is thrown, there are no changes to the view or the document it refers to. +Strong guarantee: there are no changes to the view or the document it refers to. ## Exceptions diff --git a/docs/mkdocs/docs/examples/basic_json_view__count.cpp b/docs/mkdocs/docs/examples/basic_json_view__count.cpp index 648142e21..75d82579f 100644 --- a/docs/mkdocs/docs/examples/basic_json_view__count.cpp +++ b/docs/mkdocs/docs/examples/basic_json_view__count.cpp @@ -5,13 +5,14 @@ using json_document = nlohmann::json_document; int main() { - // validate that every transaction of a batch carries a mandatory + // validate that every transaction of a batch carries exactly one // "amount" field before materializing any of them into a nlohmann::json - // value -- count() returns 0 or 1 for an object + // value -- count() counts every member with the key json_document batch = json_document::parse(R"( [ {"id": 1, "amount": 9.99}, - {"id": 2} + {"id": 2}, + {"id": 3, "amount": 5.00, "amount": 500.00} ] )"); @@ -19,9 +20,10 @@ int main() for (std::size_t i = 0; i < transactions.size(); ++i) { const auto transaction = transactions[i]; - if (transaction.count("amount") == 0) + const auto amounts = transaction.count("amount"); + if (amounts != 1) { - std::cout << "transaction " << i << " is missing \"amount\"\n"; + std::cout << "transaction " << i << " has " << amounts << " \"amount\" fields\n"; continue; } std::cout << "transaction " << i << ": " << transaction["amount"].materialize().dump() << '\n'; diff --git a/docs/mkdocs/docs/examples/basic_json_view__count.output b/docs/mkdocs/docs/examples/basic_json_view__count.output index a6344a13b..de5a0a9e8 100644 --- a/docs/mkdocs/docs/examples/basic_json_view__count.output +++ b/docs/mkdocs/docs/examples/basic_json_view__count.output @@ -1,2 +1,3 @@ transaction 0: 9.99 -transaction 1 is missing "amount" +transaction 1 has 0 "amount" fields +transaction 2 has 2 "amount" fields diff --git a/docs/mkdocs/docs/features/json_view.md b/docs/mkdocs/docs/features/json_view.md index 924af0c1d..4213d0480 100644 --- a/docs/mkdocs/docs/features/json_view.md +++ b/docs/mkdocs/docs/features/json_view.md @@ -67,9 +67,10 @@ table). [`parse_copy()`](../api/basic_json_document/parse_copy.md) always owns a !!! 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`. + If a document borrows its text, that text **must outlive the document** (and every view taken from it) and must + stay **unmodified** for as long as the document exists, since floating-point values and unescaped strings are + read from it lazily. 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: @@ -77,7 +78,7 @@ A view is valid only while all of the following hold: - the document has not been re-parsed since the view was taken (with [`read()`](../api/basic_json_document/read.md) or [`parse()`](../api/basic_json_document/parse.md) into it), - the document has not had [`shrink_to_fit()`](../api/basic_json_document/shrink_to_fit.md) called on it since, and -- if the document borrows its source text, that text is still alive. +- if the document borrows its source text, that text is still alive and unmodified. Moving the `basic_json_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()`](../api/basic_json_document/root.md) @@ -163,10 +164,10 @@ document: `#!cpp auto v = json_document::parse(text).root();` does not compile. 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), - [`count`](../api/basic_json_view/count.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`. + [`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()`](../api/basic_json_view/begin.md)/[`end()`](../api/basic_json_view/end.md) and [`items()`](../api/basic_json_view/items.md) visit *every* occurrence, in document order, and diff --git a/include/nlohmann/detail/view/lookup.hpp b/include/nlohmann/detail/view/lookup.hpp index 11ad8c97f..ee377a5e3 100644 --- a/include/nlohmann/detail/view/lookup.hpp +++ b/include/nlohmann/detail/view/lookup.hpp @@ -113,6 +113,21 @@ inline const node* find_member(const document_data& d, const node* object, const return nullptr; } +/// the number of members of an object with this key (duplicates included) +inline std::size_t count_members(const document_data& d, const node* object, const char* key, std::size_t n) noexcept +{ + std::size_t result = 0; + const node* const end = document_data::child_end(object); + for (const node* m = document_data::first_child(object); m != end; m = document_data::after(m + 1)) + { + if (m->len == n && (n == 0 || std::memcmp(d.str(*m), key, n) == 0)) + { + ++result; + } + } + return result; +} + /// whether an integer type is accepted as an array index by the view's /// operator[] and at(): every integer type but bool and size_t, which has its /// own overload diff --git a/include/nlohmann/detail/view/pointer.hpp b/include/nlohmann/detail/view/pointer.hpp index c95e5a165..fb957b604 100644 --- a/include/nlohmann/detail/view/pointer.hpp +++ b/include/nlohmann/detail/view/pointer.hpp @@ -47,24 +47,24 @@ enum class index_status template index_status array_index(const StringType& s, std::size_t& idx) noexcept { + // as basic_json: the empty token and "-" (past the last element) are + // unresolved rather than not a number + if (s.empty() || (s.size() == 1 && s[0] == '-')) + { + return index_status::unresolved; + } + if (s[0] < '0' || s[0] > '9') + { + return index_status::not_number; + } if (s.size() > 1 && s[0] == '0') { return index_status::leading_zero; } - // as basic_json: also a single character, but "-" and the empty token are - // left to the conversion below and reported as unresolved - if (!s.empty() && !(s.size() == 1 && s[0] == '-') && !(s[0] >= '0' && s[0] <= '9')) - { - return index_status::not_number; - } - if (s.empty()) - { - return index_status::unresolved; - } std::uint64_t v = 0; - for (std::size_t i = 0; i < s.size(); ++i) + for (const auto c : s) { - const auto d = static_cast(static_cast(s[i])) - '0'; + const auto d = static_cast(static_cast(c)) - '0'; if (d > 9 || v > ((std::numeric_limits::max)() - d) / 10) { return index_status::unresolved; // not a number, or beyond unsigned long long diff --git a/include/nlohmann/detail/view/value.hpp b/include/nlohmann/detail/view/value.hpp index b43a9c4df..5684ab38e 100644 --- a/include/nlohmann/detail/view/value.hpp +++ b/include/nlohmann/detail/view/value.hpp @@ -8,6 +8,7 @@ #pragma once +#include // nullptr_t, size_t #include // int64_t #include // map #include // basic_string @@ -103,6 +104,27 @@ Map map_value(const View& v) template struct is_string_key : std::is_constructible {}; +/// whether View::get() converts to T without materialize(); get_to() +/// assigns these, and passes the others to from_json +template +struct is_direct_value : std::integral_constant < bool, + std::is_arithmetic::value || std::is_same::value + || std::is_same::value + || std::is_same::value || std::is_same::value > +{}; + +template +struct is_direct_value, BasicJsonType, View> : std::true_type {}; + +template +struct is_direct_value, BasicJsonType, View> : std::true_type {}; + +template +struct is_direct_value, BasicJsonType, View> : is_string_key {}; + +template +struct is_direct_value, BasicJsonType, View> : is_string_key {}; + } // namespace view } // namespace detail NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index 60c0335aa..d754a914b 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -251,7 +251,7 @@ class basic_json_view { if (is_discarded()) { - return basic_json_view(); + return {}; } detail::view::throw_type_error(305, "cannot use operator[] with a string argument with ", type_name()); } @@ -277,7 +277,7 @@ class basic_json_view { if (is_discarded()) { - return basic_json_view(); + return {}; } detail::view::throw_type_error(305, "cannot use operator[] with a numeric argument with ", type_name()); } @@ -300,7 +300,7 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(is_discarded())) { - return basic_json_view(); + return {}; } return detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::unchecked); } @@ -473,11 +473,11 @@ class basic_json_view return !detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::contains).is_discarded(); } - /// 1 if this is an object with a member with this key, else 0 (duplicate - /// keys count once) + /// the number of members with this key (more than 1 if the key occurs + /// more than once); 0 if this is not an object size_type count(string_view_t key) const { - return contains(key) ? 1 : 0; + return is_object() ? detail::view::count_members(*m_doc, m_node, key.data(), key.size()) : 0; } size_type count(const char* key) const @@ -544,10 +544,12 @@ class basic_json_view return get_impl(detail::view::value_tag {}, detail::priority_tag<2> {}); } + /// as BasicJsonType::get_to(): the types get() converts directly are + /// assigned, all others are passed to from_json with v as it is template T& get_to(T& v) const { - v = get(); + get_to_impl(v, detail::view::is_direct_value {}); return v; } @@ -691,6 +693,21 @@ class basic_json_view return materialize().template get(); } + // --- get_to() dispatch --- + + template + void get_to_impl(T& v, std::true_type /*direct*/) const + { + v = get(); + } + + /// from_json may keep parts of v (members a JSON object lacks, say) + template + void get_to_impl(T& v, std::false_type /*direct*/) const + { + materialize().get_to(v); + } + const document_data* m_doc = nullptr; const node* m_node = nullptr; }; diff --git a/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp index 02421a122..18872bf3e 100644 --- a/tests/src/unit-json_view.cpp +++ b/tests/src/unit-json_view.cpp @@ -731,7 +731,12 @@ void check_access(const ordered_json_view& v, const ordered_json& j) ++members; const std::string key(it.key().data(), it.key().size()); CHECK(v.contains(key)); - CHECK(v.count(key) == 1); + std::size_t occurrences = 0; + for (auto other = v.begin(); other != v.end(); ++other) + { + occurrences += other.key() == it.key() ? 1 : 0; + } + CHECK(v.count(key) == occurrences); // lookups find the first member with the key, which is this one if // there is no earlier one if (std::find(keys.begin(), keys.end(), key) != keys.end()) @@ -839,7 +844,8 @@ TEST_CASE("json_view element access and iteration") CHECK(v.find("a") == v.begin()); CHECK(v.find("a").value().materialize() == 1); CHECK(v.find("b") == std::next(v.begin())); - CHECK(v.count("a") == 1); + CHECK(v.count("a") == 2); // every member, unlike materialize() + CHECK(v.count("b") == 1); CHECK(v.contains("a")); CHECK(v.value("a", 0) == 1); CHECK(v.materialize()["a"] == 3); // materialize() keeps the last value, unlike the lookups @@ -1248,6 +1254,25 @@ void from_json(const json& j, record& r) j.at("name").get_to(r.name); j.at("count").get_to(r.count); } + +struct defaults +{ + int a = 1; + int b = 2; +}; + +// sets only the members the JSON value has +void from_json(const json& j, defaults& d) +{ + if (j.contains("a")) + { + j.at("a").get_to(d.a); + } + if (j.contains("b")) + { + j.at("b").get_to(d.b); + } +} } // namespace TEST_CASE("json_view values") @@ -1369,6 +1394,14 @@ TEST_CASE("json_view values") CHECK(name == "widget"); CHECK(count == 3); + // other types go to from_json with the existing value, as basic_json::get_to() + defaults def; + def.b = 5; + const json_document partial = json_document::parse(R"({"a":4})"); + partial.root().get_to(def); + CHECK(def.a == 4); + CHECK(def.b == 5); + // a duplicate key: the last value, as parse() const json_document dup = json_document::parse(R"({"a":1,"a":2})"); CHECK((dup.root().get>() == std::map {{"a", 2}}));