diff --git a/docs/mkdocs/docs/api/basic_json_view/at.md b/docs/mkdocs/docs/api/basic_json_view/at.md index e8bd8f253..b10869b21 100644 --- a/docs/mkdocs/docs/api/basic_json_view/at.md +++ b/docs/mkdocs/docs/api/basic_json_view/at.md @@ -15,7 +15,7 @@ basic_json_view at(IntegerType idx) const; basic_json_view at(const json_pointer& ptr) const; ``` -1. Returns the value of the object member with key `key` -- the last one, should the key occur more than once (see +1. Returns the value of the object member with key `key` -- the first one, should the key occur more than once (see [Notes on duplicate keys](operator[].md#notes)). 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 @@ -35,7 +35,7 @@ basic_json_view at(const json_pointer& ptr) const; ## Return value -1. the value of the last member with key `key` +1. the value of the first member with key `key` 2. the element at index `idx` 3. the value `ptr` resolves to, starting at this value @@ -75,7 +75,7 @@ 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, scanning all of them, since the last match is wanted. Each comparison first checks the + 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. Objects with 128 or more members get a hash index while parsing, so that a lookup in them takes constant time on average. @@ -86,6 +86,13 @@ None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics ## 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. + Unlike [`operator[]`](operator[].md), which returns a [discarded](is_discarded.md) view for a missing key or an out-of-range index, `at` always throws -- exactly as `BasicJsonType::at` does, and with the same messages, so existing error handling written against `BasicJsonType::at` keeps working unchanged when switched to a view. This diff --git a/docs/mkdocs/docs/api/basic_json_view/begin.md b/docs/mkdocs/docs/api/basic_json_view/begin.md index f9c43802f..ae2665ee9 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 *last* member with a given key. See the +which all resolve to the *first* 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 f44021f71..52d2a4a41 100644 --- a/docs/mkdocs/docs/api/basic_json_view/contains.md +++ b/docs/mkdocs/docs/api/basic_json_view/contains.md @@ -33,7 +33,7 @@ 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, scanning all of them, since the last match is wanted. Each comparison first checks the + 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. Objects with 128 or more members get a hash index while parsing, so that a lookup in them takes constant time on average. @@ -43,6 +43,13 @@ No-throw guarantee: this function never throws exceptions. ## 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. + Overload 1 always returns `#!cpp false` when the value is not an object -- including a [discarded](is_discarded.md) view. diff --git a/docs/mkdocs/docs/api/basic_json_view/count.md b/docs/mkdocs/docs/api/basic_json_view/count.md index a20f3aaee..bf5bb1d21 100644 --- a/docs/mkdocs/docs/api/basic_json_view/count.md +++ b/docs/mkdocs/docs/api/basic_json_view/count.md @@ -24,13 +24,20 @@ 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, scanning all of them, since the last match is wanted. Each comparison first checks the key's length +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. Objects with 128 or more members get a hash index while parsing, so that a lookup in them takes constant time on average. ## 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. @@ -38,7 +45,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 (the lookup functions resolve to the *last* one). +counting every member with a matching key (the lookup functions resolve to the *first* 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 fb0561c10..723cad70b 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 last one, should the key occur more than once (see +Finds a member with key `key` -- the first 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. @@ -26,13 +26,20 @@ 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, scanning all of them, since the last match is wanted. Each comparison first checks the key's length +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. Objects with 128 or more members get a hash index while parsing, so that a lookup in them takes constant time on average. ## 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. + Unlike [`BasicJsonType::find`](../basic_json/find.md), which always returns `#!cpp end()` for a non-object type, this also does so for a [discarded](is_discarded.md) view -- there is no separate "invalid" iterator to return. diff --git a/docs/mkdocs/docs/api/basic_json_view/get.md b/docs/mkdocs/docs/api/basic_json_view/get.md index c3bb85790..d8ec333c9 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 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)). + [`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)). !!! info "No pointers, references, or implicit conversion" diff --git a/docs/mkdocs/docs/api/basic_json_view/items.md b/docs/mkdocs/docs/api/basic_json_view/items.md index 8e13852da..d54f8d723 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 *last* member with a given key. See the +[`contains`](contains.md), and [`count`](count.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" @@ -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) sees the *last* one, and so does [`materialize()`](materialize.md) -- like - [`BasicJsonType::parse()`](../basic_json/parse.md). + [`operator[]`](operator[].md) only ever sees the *first* one and [`materialize()`](materialize.md) -- like + [`BasicJsonType::parse()`](../basic_json/parse.md) -- keeps only the *last*. ```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 4337d95eb..1e40b3093 100644 --- a/docs/mkdocs/docs/api/basic_json_view/operator[].md +++ b/docs/mkdocs/docs/api/basic_json_view/operator[].md @@ -15,7 +15,7 @@ basic_json_view operator[](IntegerType idx) const; basic_json_view operator[](const json_pointer& ptr) const; ``` -1. Returns the value of the object member with key `key` -- the last one, should the key occur more than once (see +1. Returns the value of the object member with key `key` -- the first 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 template accepts every integer type except `#!cpp bool` and `#!cpp std::size_t` (`#!cpp int`, `#!cpp unsigned`, @@ -38,7 +38,7 @@ basic_json_view operator[](const json_pointer& ptr) const; ## Return value -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 +1. the value of the first 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)) @@ -80,7 +80,7 @@ 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, scanning all of them, since the last match is wanted. Each comparison first checks the + 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. Objects with 128 or more members get a hash index while parsing, so that a lookup in them takes constant time @@ -107,16 +107,18 @@ document. 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" +!!! 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 - 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 in an object without a hash index scans all members for this: it cannot - stop at the first match. The hash index of a larger object (128 members or more) leads to the last member of a key - as well. See the example below and [`size()`](size.md#notes). + 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 + `#!cpp v.materialize()["a"]` can differ. To get the value `parse()` would give, use + [`materialize()`](materialize.md) or iterate the members with [`items()`](items.md) and keep the last match. + [`begin()`](begin.md)/[`end()`](end.md) and [`items()`](items.md) iterate over *all* members, including + duplicates, in document order. See [Duplicate keys](../../features/json_view.md#duplicate-keys) and + [`size()`](size.md#notes). !!! info "JSON pointer resolution" diff --git a/docs/mkdocs/docs/api/basic_json_view/operator_eq.md b/docs/mkdocs/docs/api/basic_json_view/operator_eq.md index 6f96f4692..b43f5c633 100644 --- a/docs/mkdocs/docs/api/basic_json_view/operator_eq.md +++ b/docs/mkdocs/docs/api/basic_json_view/operator_eq.md @@ -18,6 +18,8 @@ bool operator==(const BasicJsonType& lhs, const basic_json_view& rhs); Neither overload builds a `BasicJsonType` value for a view to do the comparison (see [Notes](#notes) below). Numbers compare by value across their types (`#!cpp 1 == 1.0`), and an object compares by its members, with duplicate keys resolved exactly as `parse()` resolves them -- the last value, at the position of the first occurrence of the key. +This is not what [`operator[]`](operator[].md) and the other lookups return for a duplicate key: they find the *first* +member (see [Duplicate keys](../../features/json_view.md#duplicate-keys)). ## Parameters diff --git a/docs/mkdocs/docs/api/basic_json_view/value.md b/docs/mkdocs/docs/api/basic_json_view/value.md index 7ec353cd5..78d459ed1 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 last one, should the key occur more than once (see +1. Returns the value of the object member with key `key` -- the first 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 last member with key `key`, converted to `T`, or `default_value` +1. the first 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,7 +68,7 @@ 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, scanning all of them, since the last match is wanted. Plus the complexity of converting + another, in document order, stopping at the first match. Plus the complexity of converting the found member to `T` (see [`get`](get.md)). Objects with 128 or more members get a hash index while parsing, so that a lookup in them takes constant time on average. @@ -79,6 +79,13 @@ None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics ## 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. + !!! info "Differences to `at` and `operator[]`" Unlike [`at`](at.md), this function does not throw if `key`/`ptr` resolves to no value. Unlike diff --git a/docs/mkdocs/docs/examples/basic_json_view__items.cpp b/docs/mkdocs/docs/examples/basic_json_view__items.cpp index 6357c7e4b..3f7779c19 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[] and materialize() -- like - // basic_json::parse() -- see the last one + // the update history is visible; operator[] only ever sees the first + // one, and materialize() -- like basic_json::parse() -- keeps the last 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 << "last \"retries\" seen by operator[]: " << settings["retries"].materialize().dump() << '\n'; + std::cout << "first \"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 8f51dbf18..78609c8ee 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 -last "retries" seen by operator[]: 5 +first "retries" seen by operator[]: 1 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 732846de4..668eeb837 100644 --- a/docs/mkdocs/docs/features/json_view.md +++ b/docs/mkdocs/docs/features/json_view.md @@ -134,17 +134,8 @@ document: `#!cpp auto v = json_document::parse(text).root();` does not compile. `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 - *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 (objects with 128 members or more get a hash index that leads to the last - occurrence directly). - See the [Notes on duplicate keys](../api/basic_json_view/operator%5B%5D.md#notes) of `operator[]`. +- **Duplicate keys: lookups find the first member, `parse()` keeps the last.** See [Duplicate keys](#duplicate-keys) + below. - **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 view has no `basic_json` value to point at, so the exception is created without one, regardless of how @@ -156,6 +147,51 @@ document: `#!cpp auto v = json_document::parse(text).root();` does not compile. equal values for them, without ever building a tree to do it. For ordering, too, [`materialize()`](../api/basic_json_view/materialize.md) is the way to get a value you can compare. +## Duplicate keys + +!!! warning "A lookup in a view and in the parsed value can give different answers" + + If an object in the source text repeats a key, [`operator[]`](../api/basic_json_view/operator%5B%5D.md), + [`at`](../api/basic_json_view/at.md), [`find`](../api/basic_json_view/find.md), + [`value`](../api/basic_json_view/value.md), [`contains`](../api/basic_json_view/contains.md), + [`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`. + +[`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 +[`size()`](../api/basic_json_view/size.md) counts all of them, so `#!cpp view.size()` can be larger than +`#!cpp view.materialize().size()`. + +**Why the first?** A lookup can stop as soon as it finds a match. Returning the last member would force every lookup +to scan all members of the object, even when the key is found at the very first one: this made lookups in small +objects 1.6 to 3.4 times slower. Other zero-copy parsers that index the source text, such as yyjson and simdjson, also +return the first member. RFC 8259 only says that names within an object SHOULD be unique and that the behavior of a +receiver that sees duplicates is unpredictable, so neither choice is wrong. + +**What stays the same as `parse()`?** [`materialize()`](../api/basic_json_view/materialize.md) and +[`get>()`](../api/basic_json_view/get.md) replay every member in order, so they keep the *last* value +exactly like `#!cpp basic_json::parse()` (at the position of the first occurrence of the key, for an +[`ordered_json`](../api/ordered_json.md)). + +**How do I get the value `parse()` would give?** Either call `#!cpp view.materialize()` and look the key up in the +result, or iterate the members and keep the last match: + +```cpp +// the last member with the key "a", as parse() would keep it +json_view last; +for (const auto item : view.items()) +{ + if (item.key() == "a") + { + last = item.value(); + } +} +``` + +If you cannot trust the source text to have unique keys, check [`size()`](../api/basic_json_view/size.md) against the +number of distinct keys, or reject duplicates when iterating. + ## Getting values out without copying [`get()`](../api/basic_json_view/get.md) converts many `T` directly from the flat index, without ever building a @@ -185,8 +221,8 @@ Both results are only valid as long as the view -- and, for a string with no esc [`dump()`](../api/basic_json_view/dump.md) 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](#what-is-different) already differs -from a [`materialize()`](../api/basic_json_view/materialize.md)d value, see above. `#!cpp materialize().dump()` gives -a different result in both respects for a `json_view`. +from a [`materialize()`](../api/basic_json_view/materialize.md)d value, see [Duplicate keys](#duplicate-keys). +`#!cpp materialize().dump()` gives a different result in both respects for a `json_view`. By default, numbers are written the way [`basic_json::dump()`](../api/basic_json/dump.md) would. [`number_format::source`](../api/basic_json_view/number_format.md) instead copies every number exactly as it was diff --git a/include/nlohmann/detail/view/lookup.hpp b/include/nlohmann/detail/view/lookup.hpp index 69a0d8c3a..8729d2daf 100644 --- a/include/nlohmann/detail/view/lookup.hpp +++ b/include/nlohmann/detail/view/lookup.hpp @@ -84,9 +84,10 @@ class short_key std::uint64_t m_b = 0; }; -/// 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 +/// the key node of the first member of an object with the given key, or +/// nullptr (the search stops at the first match; materialize() and parse() +/// keep the last value of a duplicate key instead); 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 { if (NLOHMANN_VIEW_UNLIKELY(object->extra != 0)) @@ -95,7 +96,6 @@ inline const node* find_member(const document_data& d, const node* object, const } 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); @@ -103,19 +103,19 @@ 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) { - last = m; + return m; } } - return last; + return nullptr; } 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) { - last = m; + return m; } } - return last; + return nullptr; } /// whether an integer type is accepted as an array index by the view's diff --git a/include/nlohmann/detail/view/serializer.hpp b/include/nlohmann/detail/view/serializer.hpp index 7870601bc..bde8797c5 100644 --- a/include/nlohmann/detail/view/serializer.hpp +++ b/include/nlohmann/detail/view/serializer.hpp @@ -46,11 +46,6 @@ class output_buffer { const auto size = static_cast(m_pos - m_out.data()); m_out.resize(size); - // do not keep a buffer that was sized for a much larger output - if (m_out.capacity() > 1024 && m_out.capacity() / 2 > size) - { - m_out.shrink_to_fit(); - } } NLOHMANN_VIEW_ALWAYS_INLINE void reserve(std::size_t n) diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index 11ddf066c..7d66b9ded 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -236,7 +236,7 @@ class basic_json_view // element access // //////////////////// - /// the value of the member with this key (the last one, should the key + /// 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, 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. @@ -300,7 +300,7 @@ class 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 last one, should the key + /// the value of the member with this key (the first 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 @@ -358,7 +358,7 @@ class basic_json_view } /// the member with this key converted to T, or the default value if there - /// is no such member (the last one, should the key occur more than + /// is no such member (the first 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 @@ -423,7 +423,7 @@ class basic_json_view // lookup // //////////// - /// an iterator to the member with this key (the last one, should the + /// an iterator to the member with this key (the first one, should the /// key occur more than once), or end(); end() also for non-objects iterator find(string_view_t key) const { @@ -728,7 +728,7 @@ class basic_json_view return (std::min)(m_doc->size - m_node->off, static_cast(1024) + nodes * 16); } - /// the value of the last member with this key, or a discarded view + /// the value of the first 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/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 78d0820a8..a15d27053 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -3081,9 +3081,10 @@ class short_key std::uint64_t m_b = 0; }; -/// 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 +/// the key node of the first member of an object with the given key, or +/// nullptr (the search stops at the first match; materialize() and parse() +/// keep the last value of a duplicate key instead); 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 { if (NLOHMANN_VIEW_UNLIKELY(object->extra != 0)) @@ -3092,7 +3093,6 @@ inline const node* find_member(const document_data& d, const node* object, const } 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); @@ -3100,19 +3100,19 @@ 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) { - last = m; + return m; } } - return last; + return nullptr; } 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) { - last = m; + return m; } } - return last; + return nullptr; } /// whether an integer type is accepted as an array index by the view's @@ -3698,11 +3698,6 @@ class output_buffer { const auto size = static_cast(m_pos - m_out.data()); m_out.resize(size); - // do not keep a buffer that was sized for a much larger output - if (m_out.capacity() > 1024 && m_out.capacity() / 2 > size) - { - m_out.shrink_to_fit(); - } } NLOHMANN_VIEW_ALWAYS_INLINE void reserve(std::size_t n) @@ -4480,7 +4475,7 @@ class basic_json_view // element access // //////////////////// - /// the value of the member with this key (the last one, should the key + /// 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, 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. @@ -4544,7 +4539,7 @@ class 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 last one, should the key + /// the value of the member with this key (the first 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 @@ -4602,7 +4597,7 @@ class basic_json_view } /// the member with this key converted to T, or the default value if there - /// is no such member (the last one, should the key occur more than + /// is no such member (the first 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 @@ -4667,7 +4662,7 @@ class basic_json_view // lookup // //////////// - /// an iterator to the member with this key (the last one, should the + /// an iterator to the member with this key (the first one, should the /// key occur more than once), or end(); end() also for non-objects iterator find(string_view_t key) const { @@ -4972,7 +4967,7 @@ class basic_json_view return (std::min)(m_doc->size - m_node->off, static_cast(1024) + nodes * 16); } - /// the value of the last member with this key, or a discarded view + /// the value of the first 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 f381c6889..69636345b 100644 --- a/tests/src/unit-json_view.cpp +++ b/tests/src/unit-json_view.cpp @@ -577,7 +577,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 last occurrence +// document order; duplicate keys are found as their first occurrence void check_access(const ordered_json_view& v, const ordered_json& j) { REQUIRE(v.type() == j.type()); @@ -618,23 +618,13 @@ 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()) - { - 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) + // 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()) { continue; } + keys.push_back(key); CHECK(v.find(key) == it); CHECK(v[key].materialize() == it->materialize()); CHECK(v.at(key).materialize() == it.value().materialize()); @@ -725,20 +715,28 @@ TEST_CASE("json_view element access and iteration") #endif } - SECTION("duplicate keys: lookups find the last member, iteration all") + SECTION("duplicate keys: lookups find the first 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() == 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["a"].materialize() == 1); + CHECK(v.at("a").materialize() == 1); + 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.contains("a")); - CHECK(v.value("a", 0) == 3); - CHECK(v["a"].materialize() == v.materialize()["a"]); // as materialize() + CHECK(v.value("a", 0) == 1); + CHECK(v.materialize()["a"] == 3); // materialize() keeps the last value, unlike the lookups + // JSON pointers resolve to the first member at every level + const json_document dp = json_document::parse(R"({"a":{"b":1},"a":{"b":2,"b":3}})"); + CHECK(dp.root()[json::json_pointer("/a/b")].materialize() == 1); + CHECK(dp.root().at(json::json_pointer("/a/b")).materialize() == 1); + CHECK(dp.root().value(json::json_pointer("/a/b"), 0) == 1); + CHECK(dp.root().materialize()[json::json_pointer("/a/b")] == 3); + // get keeps the last value, as materialize() + CHECK((v.get>() == std::map {{"a", 3}, {"b", 2}})); // keys of every length class (the 16-byte short compare and memcmp) for (const std::size_t n : { @@ -748,10 +746,10 @@ TEST_CASE("json_view element access and iteration") 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].materialize() == 1); + CHECK(dk.root().at(key).materialize() == 1); + CHECK(dk.root().find(key) == dk.root().begin()); + CHECK(dk.root().value(key, 0) == 1); CHECK(dk.root()[key + "x"].materialize() == 2); } std::string order; @@ -1463,6 +1461,8 @@ TEST_CASE("json_view dump") const json_document d = json_document::parse(R"({"b": 1, "a": 2, "b": 3})"); CHECK(d.root().dump() == R"({"b":1,"a":2,"b":3})"); CHECK(d.root().dump(1) == "{\n \"b\": 1,\n \"a\": 2,\n \"b\": 3\n}"); + // dump() writes all members, though the lookup finds the first + CHECK(d.root()["b"].dump() == "1"); } SECTION("deep nesting") @@ -1585,6 +1585,9 @@ TEST_CASE("json_view comparison") const ordered_json_document dup = ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})"); const ordered_json_document last = ordered_json_document::parse(R"({"a": 3, "b": 2})"); CHECK(dup.root() == last.root()); + // (== compares as parse() resolves duplicates, a lookup finds the first) + CHECK(dup.root()["a"].materialize() == 1); + CHECK(dup.root()["a"] != last.root()["a"]); const ordered_json_document ab = ordered_json_document::parse(R"({"a": 1, "b": 2})"); const ordered_json_document ba = ordered_json_document::parse(R"({"b": 2, "a": 1})"); CHECK(ab.root() != ba.root());