From c7df23666f1f6ab834e4bb60fda26785c0c00743 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 9 Oct 2026 16:11:33 +0200 Subject: [PATCH] Make view lookups last-wins and chained access safe * Lookups (operator[], at, find, contains, count, value, JSON pointers) return the last member of a duplicate key, as materialize() and parse() keep it. * operator[] on a discarded view returns a discarded view instead of throwing, so v["a"]["b"] is safe for a missing "a". * operator[] and at() take any integer type (not only int and size_t), fixing ambiguous calls with unsigned, long, std::int64_t, ... * Fix the operator[] documentation, which claimed a discarded view for a type mismatch where type_error.305 is thrown. Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json_view/at.md | 15 +- docs/mkdocs/docs/api/basic_json_view/begin.md | 2 +- .../docs/api/basic_json_view/contains.md | 4 +- docs/mkdocs/docs/api/basic_json_view/count.md | 8 +- docs/mkdocs/docs/api/basic_json_view/find.md | 8 +- docs/mkdocs/docs/api/basic_json_view/get.md | 6 +- .../docs/api/basic_json_view/is_discarded.md | 5 + docs/mkdocs/docs/api/basic_json_view/items.md | 6 +- .../docs/api/basic_json_view/operator[].md | 58 +++--- docs/mkdocs/docs/api/basic_json_view/value.md | 8 +- .../docs/examples/basic_json_view__items.cpp | 6 +- .../examples/basic_json_view__items.output | 2 +- docs/mkdocs/docs/features/json_view.md | 10 +- include/nlohmann/detail/view/lookup.hpp | 34 +++- include/nlohmann/json_view.hpp | 50 ++++-- tests/src/unit-json_view.cpp | 168 +++++++++++++++++- 16 files changed, 306 insertions(+), 84 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json_view/at.md b/docs/mkdocs/docs/api/basic_json_view/at.md index d1b1732bb..b5deab72c 100644 --- a/docs/mkdocs/docs/api/basic_json_view/at.md +++ b/docs/mkdocs/docs/api/basic_json_view/at.md @@ -8,15 +8,18 @@ basic_json_view at(const string_t& key) const; // (2) basic_json_view at(size_type idx) const; -basic_json_view at(int idx) const; +template +basic_json_view at(IntegerType idx) const; // (3) basic_json_view at(const json_pointer& ptr) const; ``` -1. Returns the value of the object member with key `key` -- the first one, should the key occur more than once (see +1. Returns the value of the object member with key `key` -- the last one, should the key occur more than once (see [Notes on duplicate keys](operator[].md#notes)). -2. Returns the array element at index `idx`. +2. Returns the array element at index `idx`. The template accepts every integer type except `#!cpp bool` and + `#!cpp std::size_t` and forwards to the `size_type` overload, as for [`operator[]`](operator[].md); a negative + `idx` is out of range. 3. Returns the value a JSON pointer `ptr` refers to, starting at this value. ## Parameters @@ -32,7 +35,7 @@ basic_json_view at(const json_pointer& ptr) const; ## Return value -1. the value of the first member with key `key` +1. the value of the last member with key `key` 2. the element at index `idx` 3. the value `ptr` resolves to, starting at this value @@ -72,8 +75,8 @@ None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics ## Complexity 1. 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. + another, in document order, scanning all of them, since the last match is wanted. Each comparison first checks the + key's length -- already known from the index, without reading the key bytes -- before comparing its content. 2. Linear in `idx`: elements are skipped one at a time from the first one, since they are not a fixed size in the index (unlike `BasicJsonType`'s array, which is random-access). 3. Linear in the number of reference tokens of `ptr` and, for each token, in the number of members of the object at diff --git a/docs/mkdocs/docs/api/basic_json_view/begin.md b/docs/mkdocs/docs/api/basic_json_view/begin.md index ae2665ee9..f9c43802f 100644 --- a/docs/mkdocs/docs/api/basic_json_view/begin.md +++ b/docs/mkdocs/docs/api/basic_json_view/begin.md @@ -24,7 +24,7 @@ Constant. 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), -which all resolve to the *first* member with a given key. See the +which all resolve to the *last* member with a given key. See the [Notes on duplicate keys](operator[].md#notes) of `operator[]`. Because objects are iterated in document order rather than sorted by key, the order seen here can differ from what diff --git a/docs/mkdocs/docs/api/basic_json_view/contains.md b/docs/mkdocs/docs/api/basic_json_view/contains.md index bbd791887..7c1e706cd 100644 --- a/docs/mkdocs/docs/api/basic_json_view/contains.md +++ b/docs/mkdocs/docs/api/basic_json_view/contains.md @@ -33,8 +33,8 @@ No-throw guarantee: this function never throws exceptions. ## Complexity 1. 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. + another, in document order, scanning all of them, since the last match is wanted. Each comparison first checks the + key's length -- already known from the index, without reading the key bytes -- before comparing its content. 2. Linear in the number of reference tokens of `ptr` and, for each token, in the number of members of the object at that level or the index into the array -- as for [`operator[]`](operator[].md#complexity) and [`at`](at.md#complexity) with a JSON pointer. diff --git a/docs/mkdocs/docs/api/basic_json_view/count.md b/docs/mkdocs/docs/api/basic_json_view/count.md index 6a599477e..7e0091d90 100644 --- a/docs/mkdocs/docs/api/basic_json_view/count.md +++ b/docs/mkdocs/docs/api/basic_json_view/count.md @@ -23,9 +23,9 @@ 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: as for [`ordered_json`](../ordered_json.md), members are compared one after another, +in document order, scanning all of them, since the last match is wanted. Each comparison first checks the key's length +-- already known from the index, without reading the key bytes -- before comparing its content. ## Notes @@ -36,7 +36,7 @@ Unlike [`BasicJsonType::count()`](../basic_json/count.md), whose return value ca 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, not just finding the first one. +counting every member with a matching key (the lookup functions resolve to the *last* one). ## Examples diff --git a/docs/mkdocs/docs/api/basic_json_view/find.md b/docs/mkdocs/docs/api/basic_json_view/find.md index f7ad92e9f..d2323353a 100644 --- a/docs/mkdocs/docs/api/basic_json_view/find.md +++ b/docs/mkdocs/docs/api/basic_json_view/find.md @@ -6,7 +6,7 @@ iterator find(const char* key) const; iterator find(const string_t& key) const; ``` -Finds a member with key `key` -- the first one, should the key occur more than once (see +Finds a member with key `key` -- the last one, should the key occur more than once (see [Notes on duplicate keys](operator[].md#notes)). If the value is not an object, or no member has this key, [`end()`](end.md) is returned. @@ -25,9 +25,9 @@ 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: as for [`ordered_json`](../ordered_json.md), members are compared one after another, +in document order, scanning all of them, since the last match is wanted. Each comparison first checks the key's length +-- already known from the index, without reading the key bytes -- before comparing its content. ## Notes diff --git a/docs/mkdocs/docs/api/basic_json_view/get.md b/docs/mkdocs/docs/api/basic_json_view/get.md index d8ec333c9..c3bb85790 100644 --- a/docs/mkdocs/docs/api/basic_json_view/get.md +++ b/docs/mkdocs/docs/api/basic_json_view/get.md @@ -87,9 +87,9 @@ exception thrown while converting through `materialize()` (the last bullet) is d !!! info "Duplicate keys" `#!cpp std::map`/`#!cpp std::unordered_map` conversions keep the *last* value of a repeated key, like - [`materialize()`](materialize.md) and [`BasicJsonType::parse()`](../basic_json/parse.md) do. This is the opposite - of [`operator[]`](operator[].md)/[`at`](at.md)/[`find`](find.md)/[`contains`](contains.md), which resolve to the - *first* occurrence (see the [Notes on duplicate keys](operator[].md#notes)). + [`materialize()`](materialize.md) and [`BasicJsonType::parse()`](../basic_json/parse.md) do. This is the member + [`operator[]`](operator[].md)/[`at`](at.md)/[`find`](find.md)/[`contains`](contains.md) resolve to, too (see the + [Notes on duplicate keys](operator[].md#notes)). !!! info "No pointers, references, or implicit conversion" diff --git a/docs/mkdocs/docs/api/basic_json_view/is_discarded.md b/docs/mkdocs/docs/api/basic_json_view/is_discarded.md index 4cc11077d..a7fe7fa07 100644 --- a/docs/mkdocs/docs/api/basic_json_view/is_discarded.md +++ b/docs/mkdocs/docs/api/basic_json_view/is_discarded.md @@ -9,6 +9,10 @@ view (see [(constructor)](basic_json_view.md)), and for [`root()`](../basic_json that is itself [discarded](../basic_json_document/is_discarded.md) -- in particular, the root of a failed [`parse()`](../basic_json_document/parse.md) with `allow_exceptions` set to `#!cpp false`. +A discarded view is also what [`operator[]`](operator[].md) returns for a missing key, an index out of range, or a +JSON pointer that cannot be resolved, and for any access on a view that is itself discarded (so a chain such as +`#!cpp v["a"]["b"]` is safe). [`at`](at.md) throws instead. + ## Return value `#!cpp true` if the view is discarded, `#!cpp false` otherwise. @@ -45,6 +49,7 @@ site. ## See also +- [operator[]](operator[].md) - access specified element; yields a discarded view where an element is missing - [operator bool](operator_bool.md) - return whether the view refers to a value - [(constructor)](basic_json_view.md) - the default constructor creates a discarded view - [is_discarded (basic_json_document)](../basic_json_document/is_discarded.md) - return whether the last parse failed diff --git a/docs/mkdocs/docs/api/basic_json_view/items.md b/docs/mkdocs/docs/api/basic_json_view/items.md index d54f8d723..8e13852da 100644 --- a/docs/mkdocs/docs/api/basic_json_view/items.md +++ b/docs/mkdocs/docs/api/basic_json_view/items.md @@ -48,7 +48,7 @@ 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 +[`contains`](contains.md), and [`count`](count.md), which resolve to the *last* member with a given key. See the [Notes on duplicate keys](operator[].md#notes) of `operator[]`. !!! danger "Lifetime issues" @@ -63,8 +63,8 @@ occurrences of a duplicate key -- unlike [`operator[]`](operator[].md), [`at`](a The example below shows a settings object whose source text records every update to a key as a duplicate member, in the order they happened. `items()` walks all of them, so the update history is visible, while - [`operator[]`](operator[].md) only ever sees the *first* one and [`materialize()`](materialize.md) -- like - [`BasicJsonType::parse()`](../basic_json/parse.md) -- keeps only the *last*. + [`operator[]`](operator[].md) sees the *last* one, and so does [`materialize()`](materialize.md) -- like + [`BasicJsonType::parse()`](../basic_json/parse.md). ```cpp --8<-- "examples/basic_json_view__items.cpp" diff --git a/docs/mkdocs/docs/api/basic_json_view/operator[].md b/docs/mkdocs/docs/api/basic_json_view/operator[].md index 2361bf77c..30130db7c 100644 --- a/docs/mkdocs/docs/api/basic_json_view/operator[].md +++ b/docs/mkdocs/docs/api/basic_json_view/operator[].md @@ -8,16 +8,19 @@ basic_json_view operator[](const string_t& key) const; // (2) basic_json_view operator[](size_type idx) const; -basic_json_view operator[](int idx) const; +template +basic_json_view operator[](IntegerType idx) const; // (3) basic_json_view operator[](const json_pointer& ptr) const; ``` -1. Returns the value of the object member with key `key` -- the first one, should the key occur more than once (see +1. Returns the value of the object member with key `key` -- the last one, should the key occur more than once (see the [Notes](#notes) below) -- or a [discarded](is_discarded.md) view if there is no such member. -2. Returns the array element at index `idx`, or a [discarded](is_discarded.md) view if `idx` is out of range. (The - `#!cpp int` overload only exists so that an integer literal is not ambiguous between this overload and 1.) +2. Returns the array element at index `idx`, or a [discarded](is_discarded.md) view if `idx` is out of range. The + template accepts every integer type except `#!cpp bool` and `#!cpp std::size_t` (`#!cpp int`, `#!cpp unsigned`, + `#!cpp long`, `#!cpp std::int64_t`, ...) and forwards to the `size_type` overload, so that an integer argument is + not ambiguous between that overload and 1; a negative `idx` is out of range. 3. Returns the value a JSON pointer `ptr` refers to, starting at this value, or a [discarded](is_discarded.md) view wherever resolving it further is not possible without inserting into or extending the document (see [Return value](#return-value) and [Exceptions](#exceptions) below). @@ -35,10 +38,12 @@ basic_json_view operator[](const json_pointer& ptr) const; ## Return value -1. the value of the first member with key `key`, or a discarded view if `#!cpp is_object()` is `#!cpp false` or no - member has this key -2. the element at index `idx`, or a discarded view if `#!cpp is_array()` is `#!cpp false` or `#!cpp idx >= size()` -3. the value `ptr` resolves to, starting at this value, or a discarded view for exactly the reference tokens where the +1. the value of the last member with key `key`, or a discarded view if no member has this key (or if this view is + [discarded](is_discarded.md)) +2. the element at index `idx`, or a discarded view if `#!cpp idx >= size()` or `idx` is negative (or if this view is + [discarded](is_discarded.md)) +3. the value `ptr` resolves to, starting at this value, or a discarded view (also if this view is + [discarded](is_discarded.md)) for exactly the reference tokens where the **const** overload of [`BasicJsonType::operator[]`](../basic_json/operator%5B%5D.md) invokes undefined behavior for the same pointer and the same document: an object member that does not exist, or an array index that is out of range @@ -49,10 +54,12 @@ Strong exception safety: if an exception is thrown, there are no changes to the ## Exceptions -1. Throws [`type_error.305`](../../home/exceptions.md#jsonexceptiontype_error305) if the value is not an object -- +1. Throws [`type_error.305`](../../home/exceptions.md#jsonexceptiontype_error305) if the value is not an object and + not [discarded](is_discarded.md) -- the same exception, with the same message, that the **const** overload of [`BasicJsonType::operator[]`](../basic_json/operator%5B%5D.md) throws for a string argument on a non-object value. -2. Throws [`type_error.305`](../../home/exceptions.md#jsonexceptiontype_error305) if the value is not an array -- +2. Throws [`type_error.305`](../../home/exceptions.md#jsonexceptiontype_error305) if the value is not an array and + not [discarded](is_discarded.md) -- the same exception, with the same message, that the **const** overload of [`BasicJsonType::operator[]`](../basic_json/operator%5B%5D.md) throws for a numeric argument on a non-array value. 3. Throws the same exceptions, with the same messages, that the **const** overload of @@ -73,9 +80,9 @@ None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics ## Complexity 1. 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, so a key of a - different length than `key` is rejected without touching the source text. + another, in document order, scanning all of them, since the last match is wanted. Each comparison first checks the + key's length -- already known from the index, without reading the key bytes -- before comparing its content, so a + key of a different length than `key` is rejected without touching the source text. 2. Linear in `idx`: elements are skipped one at a time from the first one, since they are not a fixed size in the index (unlike `BasicJsonType`'s array, which is random-access). 3. Linear in the number of reference tokens of `ptr` and, for each token, in the number of members of the object at @@ -84,20 +91,29 @@ None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics ## Notes Unlike `BasicJsonType::operator[]`, which is undefined behavior (guarded by a -[runtime assertion](../../features/assertions.md)) for a missing key on a **const** value, this operator always -returns a safe, testable result: a [discarded](is_discarded.md) view, which is `#!cpp false` in a boolean context. +[runtime assertion](../../features/assertions.md)) for a missing key on a **const** value, this operator returns a +safe, testable result for a missing key or an index out of range: a [discarded](is_discarded.md) view, which is +`#!cpp false` in a boolean context. There is also no non-const overload that inserts a missing key or extends an array -- a view never modifies the document. +!!! info "Chained access" + + `#!cpp operator[]` on a [discarded](is_discarded.md) view returns a discarded view and does not throw, so a chain + like `#!cpp v["a"]["b"][0]` is safe even if `"a"` or `"b"` is missing: the first missing step makes the whole + result discarded, which is tested once at the end. Type errors on values that are *not* discarded still throw: a + key on an array or a primitive, or an index on an object or a primitive, is `type_error.305` as for + `BasicJsonType`. [`at`](at.md) still throws for a discarded view, as it does for a missing key. + !!! info "Duplicate keys" 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)) 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 end up keeping the *last* value for a repeated key -- there is no reason for them to stop - early. [`begin()`](begin.md)/[`end()`](end.md) and [`items()`](items.md) iterate over *all* members, including - duplicates, in document order. See the example below and [`size()`](size.md#notes). + [`contains`](contains.md), [`count`](count.md), [`value`](value.md), and JSON pointer resolution) all resolve to + the *last* member with that key. This is the member [`materialize()`](materialize.md) (and + [`BasicJsonType::parse()`](../basic_json/parse.md)) keeps, so a lookup in the view and in the materialized value + agree. [`begin()`](begin.md)/[`end()`](end.md) and [`items()`](items.md) iterate over *all* members, including + duplicates, in document order. A lookup scans all members for this: it cannot stop at the first match. See the + example below and [`size()`](size.md#notes). !!! info "JSON pointer resolution" diff --git a/docs/mkdocs/docs/api/basic_json_view/value.md b/docs/mkdocs/docs/api/basic_json_view/value.md index ff63e32f5..c13c0cd63 100644 --- a/docs/mkdocs/docs/api/basic_json_view/value.md +++ b/docs/mkdocs/docs/api/basic_json_view/value.md @@ -12,7 +12,7 @@ T value(const json_pointer& ptr, const T& default_value) const; string_t value(const json_pointer& ptr, const char* default_value) const; ``` -1. Returns the value of the object member with key `key` -- the first one, should the key occur more than once (see +1. Returns the value of the object member with key `key` -- the last one, should the key occur more than once (see [Notes on duplicate keys](operator[].md#notes)) -- converted to `T`, or `default_value` if there is no such member. 2. Returns the value a JSON pointer `ptr` refers to, starting at this value, converted to `T`, or `default_value` if `ptr` cannot be resolved. @@ -39,7 +39,7 @@ equivalent) deduce `string_t`, not `const char*`, for their return type and for ## Return value -1. the first member with key `key`, converted to `T`, or `default_value` +1. the last member with key `key`, converted to `T`, or `default_value` 2. the value `ptr` resolves to, converted to `T`, or `default_value` ## Exception safety @@ -68,8 +68,8 @@ None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics ## Complexity 1. Linear in the number of members: as for [`operator[]`](operator[].md#complexity), members are compared one after - another, in document order, stopping at the first match. Plus the complexity of converting the found member to - `T` (see [`get`](get.md)). + another, in document order, scanning all of them, since the last match is wanted. Plus the complexity of converting + the found member to `T` (see [`get`](get.md)). 2. Linear in the number of reference tokens of `ptr` and, for each token, in the number of members of the object at that level or the index into the array -- as for the [`operator[]`](operator[].md#complexity) and [`at`](at.md#complexity) overloads that take a JSON pointer. Plus the complexity of converting the resolved value diff --git a/docs/mkdocs/docs/examples/basic_json_view__items.cpp b/docs/mkdocs/docs/examples/basic_json_view__items.cpp index 3f7779c19..6357c7e4b 100644 --- a/docs/mkdocs/docs/examples/basic_json_view__items.cpp +++ b/docs/mkdocs/docs/examples/basic_json_view__items.cpp @@ -7,8 +7,8 @@ int main() { // a settings object whose source text records every update to a key as // a duplicate member. items() visits all of them, in document order, so - // the update history is visible; operator[] only ever sees the first - // one, and materialize() -- like basic_json::parse() -- keeps the last + // the update history is visible; operator[] and materialize() -- like + // basic_json::parse() -- see the last one json_document updates = json_document::parse(R"({"retries": 1, "timeout": 30, "retries": 5})"); const auto settings = updates.root(); @@ -17,6 +17,6 @@ int main() std::cout << item.key() << '=' << item.value().materialize().dump() << '\n'; } - std::cout << "first \"retries\" seen by operator[]: " << settings["retries"].materialize().dump() << '\n'; + std::cout << "last \"retries\" seen by operator[]: " << settings["retries"].materialize().dump() << '\n'; std::cout << "last \"retries\" kept by materialize(): " << settings.materialize()["retries"].dump() << '\n'; } diff --git a/docs/mkdocs/docs/examples/basic_json_view__items.output b/docs/mkdocs/docs/examples/basic_json_view__items.output index 78609c8ee..8f51dbf18 100644 --- a/docs/mkdocs/docs/examples/basic_json_view__items.output +++ b/docs/mkdocs/docs/examples/basic_json_view__items.output @@ -1,5 +1,5 @@ retries=1 timeout=30 retries=5 -first "retries" seen by operator[]: 1 +last "retries" seen by operator[]: 5 last "retries" kept by materialize(): 5 diff --git a/docs/mkdocs/docs/features/json_view.md b/docs/mkdocs/docs/features/json_view.md index 7dbea9241..06a978ee8 100644 --- a/docs/mkdocs/docs/features/json_view.md +++ b/docs/mkdocs/docs/features/json_view.md @@ -126,14 +126,20 @@ whenever any of the other conditions above was not met. 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()`](../api/basic_json_view/materialize.md)d value can print members in a different order than iterating the view they came from. +- **Chained access is safe.** [`operator[]`](../api/basic_json_view/operator%5B%5D.md) with a missing key, an index + out of range, or an unresolvable JSON pointer returns a [discarded](../api/basic_json_view/is_discarded.md) 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`](../api/basic_json_view/at.md) throws for every missing value. - **Duplicate keys are visible.** If an object in the source text repeats a key, [`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 (and [`size()`](../api/basic_json_view/size.md) counts all of them), while [`operator[]`](../api/basic_json_view/operator%5B%5D.md), [`at`](../api/basic_json_view/at.md), [`find`](../api/basic_json_view/find.md), [`contains`](../api/basic_json_view/contains.md), and [`count`](../api/basic_json_view/count.md) resolve to the - *first* occurrence, since a lookup can stop as soon as it finds a match. `basic_json::parse()` (and so - [`materialize()`](../api/basic_json_view/materialize.md)) instead keeps only the *last* value for a repeated key. + *last* occurrence -- the one `basic_json::parse()` (and so + [`materialize()`](../api/basic_json_view/materialize.md)) keeps for a repeated key -- which makes a lookup scan all + members instead of stopping at a match. See the [Notes on duplicate keys](../api/basic_json_view/operator%5B%5D.md#notes) of `operator[]`. - **No [`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md) path.** Exceptions thrown by `basic_json_view`'s own element access and lookup functions never carry the JSON Pointer path `JSON_DIAGNOSTICS` would otherwise add: the diff --git a/include/nlohmann/detail/view/lookup.hpp b/include/nlohmann/detail/view/lookup.hpp index 71338be32..a58c9a33a 100644 --- a/include/nlohmann/detail/view/lookup.hpp +++ b/include/nlohmann/detail/view/lookup.hpp @@ -11,6 +11,8 @@ #include // size_t #include // uint16_t, uint32_t, uint64_t #include // memcmp, memcpy +#include // numeric_limits +#include // integral_constant, is_integral, is_same #include #include @@ -81,12 +83,14 @@ class short_key std::uint64_t m_b = 0; }; -/// the key node of the first member of an object with the given key, or -/// nullptr; most keys are rejected by their length, from the index alone +/// the key node of the last member of an object with the given key, or +/// nullptr (the last one, as materialize() and parse() keep it); most keys are +/// rejected by their length, from the index alone inline const node* find_member(const document_data& d, const node* object, const char* key, std::size_t n) noexcept { const node* const end = document_data::child_end(object); const auto* const k = reinterpret_cast(key); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + const node* last = nullptr; if (NLOHMANN_VIEW_LIKELY(n <= 16)) { const short_key probe(k, n); @@ -94,19 +98,37 @@ inline const node* find_member(const document_data& d, const node* object, const { if (m->len == n && probe.matches(reinterpret_cast(d.str(*m)))) // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) { - return m; + last = m; } } - return nullptr; + return last; } for (const node* m = document_data::first_child(object); m != end; m = document_data::after(m + 1)) { if (m->len == n && std::memcmp(d.str(*m), key, n) == 0) { - return m; + last = m; } } - return nullptr; + return last; +} + +/// 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 +template +struct is_index_type : std::integral_constant < bool, + std::is_integral::value && !std::is_same::value && !std::is_same::value > +{}; + +/// an integer as an index: negative values, and values that do not fit a +/// size_t, map to the largest size_t (out of range for every array) +template +SizeType to_index(IntegerType idx) noexcept +{ + const IntegerType zero = 0; + const auto result = static_cast(idx); + return (idx < zero || static_cast(result) != idx) ? (std::numeric_limits::max)() : result; } /// the element of an array at an index below its size diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index b51ff446b..147a0ee4b 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -234,13 +234,18 @@ class basic_json_view // element access // //////////////////// - /// the value of the member with this key (the first one, should the key - /// occur more than once); a discarded view if there is none. Throws - /// type_error.305 if this is not an object. + /// the value of the member with this key (the last one, should the key + /// occur more than once); a discarded view if there is none, or if this + /// is a discarded view (so that v["a"]["b"] is safe). Throws type_error.305 + /// if this is any other value but an object. NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view operator[](string_view_t key) const { if (NLOHMANN_VIEW_UNLIKELY(!is_object())) { + if (is_discarded()) + { + return basic_json_view(); + } detail::view::throw_type_error(305, "cannot use operator[] with a string argument with ", type_name()); } return lookup(key); @@ -257,31 +262,43 @@ class basic_json_view } /// the element at this index; a discarded view if the index is out of - /// range. Throws type_error.305 if this is not an array. + /// range, or if this is a discarded view. Throws type_error.305 if this is + /// any other value but an array. basic_json_view operator[](size_type idx) const { if (NLOHMANN_VIEW_UNLIKELY(!is_array())) { + if (is_discarded()) + { + return basic_json_view(); + } detail::view::throw_type_error(305, "cannot use operator[] with a numeric argument with ", type_name()); } return idx < m_node->len ? basic_json_view(m_doc, detail::view::element_at(m_node, idx)) : basic_json_view(); } - /// (an int argument would be ambiguous between size_type and const char*) - basic_json_view operator[](int idx) const + /// any other integer type (int, unsigned, long, std::int64_t, ...; a + /// single overload for size_type alone would be ambiguous for all of them + /// and for const char*); negative values are out of range + template < typename IntegerType, typename std::enable_if < detail::view::is_index_type::value, int >::type = 0 > + basic_json_view operator[](IntegerType idx) const { - return operator[](static_cast(idx)); + return operator[](detail::view::to_index(idx)); } /// the value a JSON pointer refers to; a discarded view if a key is - /// missing or an index is out of range. Other errors throw what const - /// basic_json::operator[] throws. + /// missing or an index is out of range, or if this is a discarded view. + /// Other errors throw what const basic_json::operator[] throws. basic_json_view operator[](const json_pointer& ptr) const { + if (NLOHMANN_VIEW_UNLIKELY(is_discarded())) + { + return basic_json_view(); + } return detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::unchecked); } - /// the value of the member with this key (the first one, should the key + /// the value of the member with this key (the last one, should the key /// occur more than once). Throws type_error.304 if this is not an object, /// and out_of_range.403 if there is no such member. basic_json_view at(string_view_t key) const @@ -323,9 +340,12 @@ class basic_json_view return basic_json_view(m_doc, detail::view::element_at(m_node, idx)); } - basic_json_view at(int idx) const + /// any other integer type, see operator[]; negative values are out of + /// range + template < typename IntegerType, typename std::enable_if < detail::view::is_index_type::value, int >::type = 0 > + basic_json_view at(IntegerType idx) const { - return at(static_cast(idx)); + return at(detail::view::to_index(idx)); } /// the value a JSON pointer refers to; throws what basic_json::at() @@ -336,7 +356,7 @@ class basic_json_view } /// the member with this key converted to T, or the default value if there - /// is no such member (the first one, should the key occur more than + /// is no such member (the last one, should the key occur more than /// once). Throws type_error.306 if this is not an object. template < typename T, typename std::enable_if < !std::is_same::type, const char*>::value, int >::type = 0 > T value(string_view_t key, const T& default_value) const @@ -401,7 +421,7 @@ class basic_json_view // lookup // //////////// - /// an iterator to the member with this key (the first one, should the + /// an iterator to the member with this key (the last one, should the /// key occur more than once), or end(); end() also for non-objects iterator find(string_view_t key) const { @@ -579,7 +599,7 @@ class basic_json_view : m_doc(d), m_node(n) {} - /// the value of the first member with this key, or a discarded view + /// the value of the last member with this key, or a discarded view /// (object required) NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view lookup(string_view_t key) const noexcept { diff --git a/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp index e208c2abb..55da948e0 100644 --- a/tests/src/unit-json_view.cpp +++ b/tests/src/unit-json_view.cpp @@ -23,6 +23,7 @@ using nlohmann::ordered_json_view; #include #include #include +#include #include #include #include @@ -442,7 +443,7 @@ std::string exception_of(F f) // compares a view with the ordered_json value materialize() gives for it: // types, sizes, elements and members (by index, key, and iteration), in -// document order; duplicate keys are found as their first occurrence +// document order; duplicate keys are found as their last occurrence void check_access(const ordered_json_view& v, const ordered_json& j) { REQUIRE(v.type() == j.type()); @@ -483,11 +484,23 @@ void check_access(const ordered_json_view& v, const ordered_json& j) const std::string key(it.key().data(), it.key().size()); CHECK(v.contains(key)); CHECK(v.count(key) == 1); - if (std::find(keys.begin(), keys.end(), key) != keys.end()) + if (std::find(keys.begin(), keys.end(), key) == keys.end()) { - continue; // a duplicate: lookups find the first one + keys.push_back(key); + } + // lookups find the last member with the key, which is this one if + // there is no later one + auto next = it; + ++next; + bool is_last = true; + for (; next != v.end(); ++next) + { + is_last = is_last && next.key() != it.key(); + } + if (!is_last) + { + continue; } - keys.push_back(key); CHECK(v.find(key) == it); CHECK(v[key].materialize() == it->materialize()); CHECK(v.at(key).materialize() == it.value().materialize()); @@ -578,15 +591,35 @@ TEST_CASE("json_view element access and iteration") #endif } - SECTION("duplicate keys: lookups find the first member, iteration all") + SECTION("duplicate keys: lookups find the last member, iteration all") { const json_document d = json_document::parse(R"({"a":1,"b":2,"a":3})"); const json_view v = d.root(); CHECK(v.size() == 3); - CHECK(v["a"].materialize() == 1); - CHECK(v.at("a").materialize() == 1); - CHECK(v.find("a") == v.begin()); + CHECK(v["a"].materialize() == 3); + CHECK(v.at("a").materialize() == 3); + CHECK(v.find("a") == std::next(v.begin(), 2)); + CHECK(v.find("a").value().materialize() == 3); + CHECK(v.find("b") == std::next(v.begin())); CHECK(v.count("a") == 1); + CHECK(v.contains("a")); + CHECK(v.value("a", 0) == 3); + CHECK(v["a"].materialize() == v.materialize()["a"]); // as materialize() + // keys of every length class (the 16-byte short compare and memcmp) + for (const std::size_t n : + { + 0u, 1u, 3u, 7u, 8u, 15u, 16u, 17u, 40u + }) + { + const std::string key(n, 'k'); + const json_document dk = json_document::parse("{\"" + key + "\":1,\"" + key + "x\":2,\"" + key + "\":3,\"" + key + "\":4}"); + CAPTURE(n) + CHECK(dk.root()[key].materialize() == 4); + CHECK(dk.root().at(key).materialize() == 4); + CHECK(dk.root().find(key) == std::next(dk.root().begin(), 3)); + CHECK(dk.root().value(key, 0) == 4); + CHECK(dk.root()[key + "x"].materialize() == 2); + } std::string order; for (auto it = v.begin(); it != v.end(); ++it) { @@ -644,7 +677,124 @@ TEST_CASE("json_view element access and iteration") const json_view invalid{}; CHECK(invalid.begin() == invalid.end()); CHECK(std::string(invalid.type_name()) == "discarded"); - CHECK_THROWS_WITH_AS(invalid["a"], "[json.exception.type_error.305] cannot use operator[] with a string argument with discarded", json::type_error&); + } + + SECTION("chained access is safe: operator[] of a discarded view is discarded") + { + const json_document d = json_document::parse(R"({"a":{"b":[10,20]},"s":"str"})"); + const json_view v = d.root(); + // missing keys and indexes + CHECK(!v["x"]); + CHECK(!v["x"]["y"]); + CHECK(!v["x"]["y"]["z"]); + CHECK(!v["x"][0]); + CHECK(!v["x"][0u][1L]); + CHECK(!v["a"]["b"][2]); + CHECK(!v["a"]["b"][2]["c"]); + CHECK(!v["a"]["b"][2][json_view::json_pointer("/c")]); + CHECK(!v["x"][json_view::json_pointer("/a/b")]); + CHECK(!v["x"][json_view::json_pointer("")]); + CHECK(v["x"]["y"].is_discarded()); + CHECK(!v["x"][std::string("y")]); + // a resolvable path still resolves + CHECK(v["a"]["b"][1].materialize() == 20); + CHECK(v[json_view::json_pointer("/a/b/1")].materialize() == 20); + // the discarded view of an unresolved pointer is discarded too + CHECK(!v[json_view::json_pointer("/x/y")]["z"]); + CHECK(!v[json_view::json_pointer("/a/b/5")][0]); +#if !defined(JSON_NOEXCEPTION) + // type errors on values that are not discarded stay + CHECK_THROWS_WITH_AS(v[0], "[json.exception.type_error.305] cannot use operator[] with a numeric argument with object", json::type_error&); + CHECK_THROWS_WITH_AS(v["a"]["b"]["c"], "[json.exception.type_error.305] cannot use operator[] with a string argument with array", json::type_error&); + CHECK_THROWS_WITH_AS(v["s"]["c"], "[json.exception.type_error.305] cannot use operator[] with a string argument with string", json::type_error&); + CHECK_THROWS_WITH_AS(v["s"][0], "[json.exception.type_error.305] cannot use operator[] with a numeric argument with string", json::type_error&); + CHECK_THROWS_AS(v["a"]["b"][0]["c"], json::type_error&); + CHECK_THROWS_AS(v["s"][json_view::json_pointer("/x")], json::out_of_range&); + // at() keeps throwing on a discarded view + const json_view invalid{}; + CHECK(!invalid["a"]); + CHECK(!invalid[0]); + CHECK(!invalid[json_view::json_pointer("/a")]); + CHECK_THROWS_WITH_AS(invalid.at("a"), "[json.exception.type_error.304] cannot use at() with discarded", json::type_error&); + CHECK_THROWS_WITH_AS(invalid.at(0), "[json.exception.type_error.304] cannot use at() with discarded", json::type_error&); + CHECK_THROWS_AS(v.at("x").at("y"), json::out_of_range&); + CHECK_THROWS_AS(v["x"].at("y"), json::type_error&); + CHECK_THROWS_AS(v.at(json_view::json_pointer("/x/y")), json::out_of_range&); + CHECK_THROWS_AS(v["x"].at(json_view::json_pointer("/y")), json::out_of_range&); +#endif + } + + SECTION("integer types as array indexes") + { + const json_document d = json_document::parse("[10,20,30]"); + const json_view v = d.root(); + const json j = v.materialize(); + // (compile-time: no overload is ambiguous) + CHECK(v[0].materialize() == 10); + CHECK(v[1].materialize() == 20); + CHECK(v[0u].materialize() == 10); + CHECK(v[1u].materialize() == 20); + CHECK(v[1L].materialize() == 20); + CHECK(v[2UL].materialize() == 30); + CHECK(v[1LL].materialize() == 20); + CHECK(v[2ULL].materialize() == 30); + CHECK(v[static_cast(1)].materialize() == 20); + CHECK(v[static_cast(2)].materialize() == 30); + CHECK(v[static_cast(1)].materialize() == 20); + CHECK(v[static_cast(2)].materialize() == 30); + CHECK(v[std::int8_t(1)].materialize() == 20); + CHECK(v[std::int16_t(2)].materialize() == 30); + CHECK(v[std::int32_t(1)].materialize() == 20); + CHECK(v[std::int64_t(2)].materialize() == 30); + CHECK(v[std::uint32_t(0)].materialize() == 10); + CHECK(v[std::uint64_t(1)].materialize() == 20); + CHECK(v[std::size_t(2)].materialize() == 30); + CHECK(v[std::ptrdiff_t(1)].materialize() == 20); + CHECK(j[0u] == 10); // as basic_json + + CHECK(v.at(0).materialize() == 10); + CHECK(v.at(1u).materialize() == 20); + CHECK(v.at(1L).materialize() == 20); + CHECK(v.at(2LL).materialize() == 30); + CHECK(v.at(2ULL).materialize() == 30); + CHECK(v.at(static_cast(1)).materialize() == 20); + CHECK(v.at(static_cast(2)).materialize() == 30); + CHECK(v.at(std::int32_t(0)).materialize() == 10); + CHECK(v.at(std::uint32_t(0)).materialize() == 10); + CHECK(v.at(std::int64_t(0)).materialize() == 10); + CHECK(v.at(std::uint64_t(1)).materialize() == 20); + CHECK(v.at(std::size_t(2)).materialize() == 30); + CHECK(j.at(std::uint32_t(0)) == 10); // as basic_json + + // out of range, including negative values (no wrap-around) + CHECK(!v[3]); + CHECK(!v[3u]); + CHECK(!v[3L]); + CHECK(!v[-1]); + CHECK(!v[-1L]); + CHECK(!v[-1LL]); + CHECK(!v[static_cast(-1)]); + CHECK(!v[std::int64_t(-3)]); + CHECK(!v[(std::numeric_limits::min)()]); + CHECK(!v[(std::numeric_limits::max)()]); + CHECK(!v[(std::numeric_limits::max)()]); + CHECK(!v[std::numeric_limits::max()]); +#if !defined(JSON_NOEXCEPTION) + CHECK_THROWS_WITH_AS(v.at(3), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&); + CHECK_THROWS_WITH_AS(v.at(3u), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&); + CHECK_THROWS_WITH_AS(v.at(std::int64_t(3)), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&); + CHECK_THROWS_AS(v.at(-1), json::out_of_range&); + CHECK_THROWS_AS(v.at(-1L), json::out_of_range&); + CHECK_THROWS_AS(v.at(std::int64_t(-1)), json::out_of_range&); + CHECK_THROWS_AS(v.at((std::numeric_limits::min)()), json::out_of_range&); + CHECK_THROWS_AS(v.at((std::numeric_limits::max)()), json::out_of_range&); + // not an array + const json_document o = json_document::parse("{}"); + CHECK_THROWS_AS(o.root()[0u], json::type_error&); + CHECK_THROWS_AS(o.root()[1L], json::type_error&); + CHECK_THROWS_AS(o.root().at(std::uint32_t(0)), json::type_error&); + CHECK_THROWS_AS(o.root().at(std::int64_t(0)), json::type_error&); +#endif } SECTION("iterators")