Return the first member of duplicate keys from lookups again

Looking up the last member of a duplicate key cannot stop at a match, so
every lookup scanned the whole object (1.6 to 3.4 times slower for small
objects). operator[](key), at, find, value, contains, count, and JSON
pointer resolution return the first member again, as yyjson and simdjson
do; materialize() and get<map>() keep the last value, as parse().

The documentation says so in the feature page and on each lookup page,
and explains how to get the value parse() would give. The integer index
templates and the discarded chaining of operator[] stay.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann committed 2026-10-11 10:28:03 +02:00
1 parent 10ec5a0381
commit decf6de9c7
15 files changed
+153 -80

No files matched your search

+10 -3
View File
@@ -15,7 +15,7 @@ basic_json_view at(IntegerType idx) const;
basic_json_view at(const json_pointer& ptr) 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)). [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 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 `#!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 ## 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` 2. the element at index `idx`
3. the value `ptr` resolves to, starting at this value 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 ## Complexity
1. Linear in the number of members: as for [`ordered_json`](../ordered_json.md), members are compared one after 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. 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 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). index (unlike `BasicJsonType`'s array, which is random-access).
@@ -84,6 +84,13 @@ None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics
## Notes ## 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 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 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 existing error handling written against `BasicJsonType::at` keeps working unchanged when switched to a view. This
@@ -24,7 +24,7 @@ Constant.
For an object, iteration visits **every** member, including all occurrences of a duplicate key -- unlike 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), [`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[]`. [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 Because objects are iterated in document order rather than sorted by key, the order seen here can differ from what
@@ -33,7 +33,7 @@ No-throw guarantee: this function never throws exceptions.
## Complexity ## Complexity
1. Linear in the number of members: as for [`ordered_json`](../ordered_json.md), members are compared one after 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. 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 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 that level or the index into the array -- as for [`operator[]`](operator[].md#complexity) and
@@ -41,6 +41,13 @@ No-throw guarantee: this function never throws exceptions.
## Notes ## 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) Overload 1 always returns `#!cpp false` when the value is not an object -- including a [discarded](is_discarded.md)
view. view.
@@ -24,11 +24,18 @@ No-throw guarantee: this function never throws exceptions.
## Complexity ## Complexity
Linear in the number of members: as for [`ordered_json`](../ordered_json.md), members are compared one after another, 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. -- already known from the index, without reading the key bytes -- before comparing its content.
## Notes ## 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) This method always returns `#!cpp 0` when the value is not an object -- including a [discarded](is_discarded.md)
view. view.
@@ -36,7 +43,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 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 [`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 [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 ## Examples
+9 -2
View File
@@ -6,7 +6,7 @@ iterator find(const char* key) const;
iterator find(const string_t& 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, [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. [`end()`](end.md) is returned.
@@ -26,11 +26,18 @@ No-throw guarantee: this function never throws exceptions.
## Complexity ## Complexity
Linear in the number of members: as for [`ordered_json`](../ordered_json.md), members are compared one after another, 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. -- already known from the index, without reading the key bytes -- before comparing its content.
## Notes ## 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 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. also does so for a [discarded](is_discarded.md) view -- there is no separate "invalid" iterator to return.
+3 -3
View File
@@ -87,9 +87,9 @@ exception thrown while converting through `materialize()` (the last bullet) is d
!!! info "Duplicate keys" !!! info "Duplicate keys"
`#!cpp std::map`/`#!cpp std::unordered_map` conversions keep the *last* value of a repeated key, like `#!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 [`materialize()`](materialize.md) and [`BasicJsonType::parse()`](../basic_json/parse.md) do. This is the opposite
[`operator[]`](operator[].md)/[`at`](at.md)/[`find`](find.md)/[`contains`](contains.md) resolve to, too (see the of [`operator[]`](operator[].md)/[`at`](at.md)/[`find`](find.md)/[`contains`](contains.md), which resolve to the
[Notes on duplicate keys](operator[].md#notes)). *first* occurrence (see the [Notes on duplicate keys](operator[].md#notes)).
!!! info "No pointers, references, or implicit conversion" !!! info "No pointers, references, or implicit conversion"
@@ -48,7 +48,7 @@ Constant.
As for [`begin()`](begin.md)/[`end()`](end.md), `items()` visits **every** member of an object, including all 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), 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[]`. [Notes on duplicate keys](operator[].md#notes) of `operator[]`.
!!! danger "Lifetime issues" !!! 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 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 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 [`operator[]`](operator[].md) only ever sees the *first* one and [`materialize()`](materialize.md) -- like
[`BasicJsonType::parse()`](../basic_json/parse.md). [`BasicJsonType::parse()`](../basic_json/parse.md) -- keeps only the *last*.
```cpp ```cpp
--8<-- "examples/basic_json_view__items.cpp" --8<-- "examples/basic_json_view__items.cpp"
@@ -15,7 +15,7 @@ basic_json_view operator[](IntegerType idx) const;
basic_json_view operator[](const json_pointer& ptr) 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. 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 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`, 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 ## 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)) [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 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)) [discarded](is_discarded.md))
@@ -80,7 +80,7 @@ None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics
## Complexity ## Complexity
1. Linear in the number of members: as for [`ordered_json`](../ordered_json.md), members are compared one after 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'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. 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 2. Linear in `idx`: elements are skipped one at a time from the first one, since they are not a fixed size in the
@@ -105,15 +105,18 @@ document.
key on an array or a primitive, or an index on an object or a primitive, is `type_error.305` as for 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. `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), 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), [`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 the *first* member with that key, because a lookup can stop as soon as it finds a match. This is different from
[`BasicJsonType::parse()`](../basic_json/parse.md)) keeps, so a lookup in the view and in the materialized value [`materialize()`](materialize.md) (and [`BasicJsonType::parse()`](../basic_json/parse.md)), which replay every
agree. [`begin()`](begin.md)/[`end()`](end.md) and [`items()`](items.md) iterate over *all* members, including member in order and so keep the *last* value for a repeated key, so `#!cpp v["a"]` and
duplicates, in document order. A lookup scans all members for this: it cannot stop at the first match. See the `#!cpp v.materialize()["a"]` can differ. To get the value `parse()` would give, use
example below and [`size()`](size.md#notes). [`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" !!! info "JSON pointer resolution"
+10 -3
View File
@@ -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; 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. [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 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. `ptr` cannot be resolved.
@@ -39,7 +39,7 @@ equivalent) deduce `string_t`, not `const char*`, for their return type and for
## Return value ## 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` 2. the value `ptr` resolves to, converted to `T`, or `default_value`
## Exception safety ## Exception safety
@@ -68,7 +68,7 @@ None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics
## Complexity ## Complexity
1. Linear in the number of members: as for [`operator[]`](operator[].md#complexity), members are compared one after 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)). 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 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 that level or the index into the array -- as for the [`operator[]`](operator[].md#complexity) and
@@ -77,6 +77,13 @@ None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics
## Notes ## 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[]`" !!! info "Differences to `at` and `operator[]`"
Unlike [`at`](at.md), this function does not throw if `key`/`ptr` resolves to no value. Unlike Unlike [`at`](at.md), this function does not throw if `key`/`ptr` resolves to no value. Unlike
@@ -7,8 +7,8 @@ int main()
{ {
// a settings object whose source text records every update to a key as // 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 // a duplicate member. items() visits all of them, in document order, so
// the update history is visible; operator[] and materialize() -- like // the update history is visible; operator[] only ever sees the first
// basic_json::parse() -- see the last one // one, and materialize() -- like basic_json::parse() -- keeps the last
json_document updates = json_document::parse(R"({"retries": 1, "timeout": 30, "retries": 5})"); json_document updates = json_document::parse(R"({"retries": 1, "timeout": 30, "retries": 5})");
const auto settings = updates.root(); const auto settings = updates.root();
@@ -17,6 +17,6 @@ int main()
std::cout << item.key() << '=' << item.value().materialize().dump() << '\n'; 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'; std::cout << "last \"retries\" kept by materialize(): " << settings.materialize()["retries"].dump() << '\n';
} }
@@ -1,5 +1,5 @@
retries=1 retries=1
timeout=30 timeout=30
retries=5 retries=5
last "retries" seen by operator[]: 5 first "retries" seen by operator[]: 1
last "retries" kept by materialize(): 5 last "retries" kept by materialize(): 5
+47 -10
View File
@@ -134,16 +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 `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 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. [`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, - **Duplicate keys: lookups find the first member, `parse()` keeps the last.** See [Duplicate keys](#duplicate-keys)
[`begin()`](../api/basic_json_view/begin.md)/[`end()`](../api/basic_json_view/end.md) and below.
[`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.
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 - **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 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 view has no `basic_json` value to point at, so the exception is created without one, regardless of how
@@ -151,6 +143,51 @@ document: `#!cpp auto v = json_document::parse(text).root();` does not compile.
- **`dump()` and comparison are not (yet) provided** by `basic_json_view`. For now, - **`dump()` and comparison are not (yet) provided** by `basic_json_view`. For now,
[`materialize()`](../api/basic_json_view/materialize.md) is the way to get a value you can do those things with. [`materialize()`](../api/basic_json_view/materialize.md) is the way to get a value you can do those things with.
## 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<std::map<...>>()`](../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 ## Getting values out without copying
[`get<T>()`](../api/basic_json_view/get.md) converts many `T` directly from the flat index, without ever building a [`get<T>()`](../api/basic_json_view/get.md) converts many `T` directly from the flat index, without ever building a
+8 -8
View File
@@ -83,14 +83,14 @@ class short_key
std::uint64_t m_b = 0; std::uint64_t m_b = 0;
}; };
/// the key node of the last member of an object with the given key, or /// the key node of the first member of an object with the given key, or
/// nullptr (the last one, as materialize() and parse() keep it); most keys are /// nullptr (the search stops at the first match; materialize() and parse()
/// rejected by their length, from the index alone /// 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 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 node* const end = document_data::child_end(object);
const auto* const k = reinterpret_cast<const unsigned char*>(key); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) const auto* const k = reinterpret_cast<const unsigned char*>(key); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
const node* last = nullptr;
if (NLOHMANN_VIEW_LIKELY(n <= 16)) if (NLOHMANN_VIEW_LIKELY(n <= 16))
{ {
const short_key probe(k, n); const short_key probe(k, n);
@@ -98,19 +98,19 @@ inline const node* find_member(const document_data& d, const node* object, const
{ {
if (m->len == n && probe.matches(reinterpret_cast<const unsigned char*>(d.str(*m)))) // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) if (m->len == n && probe.matches(reinterpret_cast<const unsigned char*>(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)) 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) 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 /// whether an integer type is accepted as an array index by the view's
+5 -5
View File
@@ -240,7 +240,7 @@ class basic_json_view
// element access // // 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 /// 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 /// is a discarded view (so that v["a"]["b"] is safe). Throws type_error.305
/// if this is any other value but an object. /// if this is any other value but an object.
@@ -304,7 +304,7 @@ class basic_json_view
return detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::unchecked); 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, /// 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. /// and out_of_range.403 if there is no such member.
basic_json_view at(string_view_t key) const basic_json_view at(string_view_t key) const
@@ -362,7 +362,7 @@ class basic_json_view
} }
/// the member with this key converted to T, or the default value if there /// 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. /// once). Throws type_error.306 if this is not an object.
template < typename T, typename std::enable_if < !std::is_same<typename std::decay<T>::type, const char*>::value, int >::type = 0 > template < typename T, typename std::enable_if < !std::is_same<typename std::decay<T>::type, const char*>::value, int >::type = 0 >
T value(string_view_t key, const T& default_value) const T value(string_view_t key, const T& default_value) const
@@ -427,7 +427,7 @@ class basic_json_view
// lookup // // 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 /// key occur more than once), or end(); end() also for non-objects
iterator find(string_view_t key) const iterator find(string_view_t key) const
{ {
@@ -605,7 +605,7 @@ class basic_json_view
: m_doc(d), m_node(n) : m_doc(d), m_node(n)
{} {}
/// 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) /// (object required)
NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view lookup(string_view_t key) const noexcept NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view lookup(string_view_t key) const noexcept
{ {
+24 -26
View File
@@ -583,7 +583,7 @@ std::string exception_of(F f)
// compares a view with the ordered_json value materialize() gives for it: // compares a view with the ordered_json value materialize() gives for it:
// types, sizes, elements and members (by index, key, and iteration), in // 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) void check_access(const ordered_json_view& v, const ordered_json& j)
{ {
REQUIRE(v.type() == j.type()); REQUIRE(v.type() == j.type());
@@ -624,23 +624,13 @@ void check_access(const ordered_json_view& v, const ordered_json& j)
const std::string key(it.key().data(), it.key().size()); const std::string key(it.key().data(), it.key().size());
CHECK(v.contains(key)); CHECK(v.contains(key));
CHECK(v.count(key) == 1); CHECK(v.count(key) == 1);
if (std::find(keys.begin(), keys.end(), key) == keys.end()) // lookups find the first member with the key, which is this one if
{ // there is no earlier one
keys.push_back(key); if (std::find(keys.begin(), keys.end(), key) != keys.end())
}
// 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; continue;
} }
keys.push_back(key);
CHECK(v.find(key) == it); CHECK(v.find(key) == it);
CHECK(v[key].materialize() == it->materialize()); CHECK(v[key].materialize() == it->materialize());
CHECK(v.at(key).materialize() == it.value().materialize()); CHECK(v.at(key).materialize() == it.value().materialize());
@@ -731,20 +721,28 @@ TEST_CASE("json_view element access and iteration")
#endif #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_document d = json_document::parse(R"({"a":1,"b":2,"a":3})");
const json_view v = d.root(); const json_view v = d.root();
CHECK(v.size() == 3); CHECK(v.size() == 3);
CHECK(v["a"].materialize() == 3); CHECK(v["a"].materialize() == 1);
CHECK(v.at("a").materialize() == 3); CHECK(v.at("a").materialize() == 1);
CHECK(v.find("a") == std::next(v.begin(), 2)); CHECK(v.find("a") == v.begin());
CHECK(v.find("a").value().materialize() == 3); CHECK(v.find("a").value().materialize() == 1);
CHECK(v.find("b") == std::next(v.begin())); CHECK(v.find("b") == std::next(v.begin()));
CHECK(v.count("a") == 1); CHECK(v.count("a") == 1);
CHECK(v.contains("a")); CHECK(v.contains("a"));
CHECK(v.value("a", 0) == 3); CHECK(v.value("a", 0) == 1);
CHECK(v["a"].materialize() == v.materialize()["a"]); // as materialize() 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<map> keeps the last value, as materialize()
CHECK((v.get<std::map<std::string, int>>() == std::map<std::string, int> {{"a", 3}, {"b", 2}}));
// keys of every length class (the 16-byte short compare and memcmp) // keys of every length class (the 16-byte short compare and memcmp)
for (const std::size_t n : for (const std::size_t n :
{ {
@@ -754,10 +752,10 @@ TEST_CASE("json_view element access and iteration")
const std::string key(n, 'k'); const std::string key(n, 'k');
const json_document dk = json_document::parse("{\"" + key + "\":1,\"" + key + "x\":2,\"" + key + "\":3,\"" + key + "\":4}"); const json_document dk = json_document::parse("{\"" + key + "\":1,\"" + key + "x\":2,\"" + key + "\":3,\"" + key + "\":4}");
CAPTURE(n) CAPTURE(n)
CHECK(dk.root()[key].materialize() == 4); CHECK(dk.root()[key].materialize() == 1);
CHECK(dk.root().at(key).materialize() == 4); CHECK(dk.root().at(key).materialize() == 1);
CHECK(dk.root().find(key) == std::next(dk.root().begin(), 3)); CHECK(dk.root().find(key) == dk.root().begin());
CHECK(dk.root().value(key, 0) == 4); CHECK(dk.root().value(key, 0) == 1);
CHECK(dk.root()[key + "x"].materialize() == 2); CHECK(dk.root()[key + "x"].materialize() == 2);
} }
std::string order; std::string order;