Compare commits

...
Author SHA1 Message Date
Niels Lohmann d2e05c8b45 Fix rollback of pointer operator[] for unusual object and string types
Follow-up to the strong exception guarantee of the non-const
operator[](json_pointer):

- get_unchecked_create assumed that emplace() always inserts the key that
  the walk did not find. For object types whose lookup and emplace()
  disagree (e.g. a copied fifo_map), nothing was inserted, the rollback
  erased the existing member, and the walk continued with a value of the
  wrong type (assertion, type_error.305, or a crash). It now checks the
  result of emplace(), arms the rollback only if a key was inserted, and
  otherwise continues the walk with the member found, as basic_json's
  operator[] does: such pointers throw out_of_range.404 and leave the value
  unchanged, as before the strong guarantee.
- The rollback erased the inserted key by key, which converts string_t to
  object_t::key_type and may allocate inside a noexcept function
  (std::terminate on bad_alloc with custom key types). It now erases by the
  iterator emplace() returned, and shrinks a grown array with erase() at
  the end instead of resize(). The documentation states the remaining
  requirement for custom template parameters.
- Creating values no longer calls string_t::begin(), so the non-const
  operator[](json_pointer) compiles with string types that lack it (as the
  alternative string type of the tests).
- Correct the @throw lists of get_unchecked and get_unchecked_create.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 17:13:52 +01:00
Niels Lohmann d1566fe9e8 Make the strongly exception-safe pointer operator[] faster than before
The previous commit made get_unchecked strongly exception safe but slower
with GCC 16 when it creates values (up to +6 % instructions for appending
with "-"). This restructures it so that no benchmark row is slower than
before the strong guarantee, with GCC 16, Clang 22 and Apple Clang.

- get_unchecked walks existing values in one loop that inserts nothing.
  The last reference token is resolved in that walk: "-" appends with
  emplace_back(), an index grows the array with operator[], and a missing
  key is inserted with emplace_hint() at the position the walk's
  lower_bound() found, so the key is searched once.
- With std::less<string_t> or std::less<> as comparator, the walk checks
  the key at the lower bound for equality instead of equivalence, which is
  the same for these comparators and cheaper.
- get_unchecked_create (not inlined) handles all other changes. Its guard
  swaps a null value with a fresh null value instead of copying it and
  reverts the change in a non-inlined function, so a successful call pays
  one compare for it. As every null value converted afterwards is a fresh
  one, it is converted in place instead of assigning a new array or object.
- Keys are added to new objects with emplace(), which also replaces the
  basic_json::operator[] call for ordered_json.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 17:13:52 +01:00
Niels Lohmann 3351a8795b Keep the value unchanged when operator[] with a JSON pointer throws
The non-const operator[](json_pointer) is documented with strong exception
safety, but get_unchecked created intermediate values while walking the
pointer and kept them when a later reference token failed, e.g.
json j; j["/a/99999999999999999999999"_json_pointer] threw 404 and left
{"a":[]}; padding an existing array or appending with "-" behaved alike,
and so did std::length_error/std::bad_alloc from a huge index.

get_unchecked now only walks existing values up to the last reference
token. The last token's key insertion or array growth is strongly
exception safe by itself. Any other change goes through
get_unchecked_create, which records the one change it makes to an existing
value (null converted, key inserted, array grown) in a RAII guard and
reverts it if a later step throws; everything else it creates lies below
that change. The guard needs no try/catch, so JSON_NOEXCEPTION builds are
unaffected. Successful calls create the same values as before.

Docs: state the guarantee in the exceptions list and version history of
operator[], fix the complexity of the pointer overloads of operator[] and
at, and warn that padding arrays up to an untrusted index allocates memory
proportional to the index (operator[].md, untrusted_input.md).

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 17:13:52 +01:00
Niels Lohmann 81d66291c5 Create objects for pointer tokens that cannot be array indices (#5358)
* create objects for tokens that cannot be array indices (#5357)

When operator[](json_pointer) traverses a level that does not exist yet,
the null value is turned into an array or an object depending on the
reference token. The check only tested whether all characters are digits,
so tokens that can never be a valid array index selected an array and
then failed:

- "01" (and any other token with a leading '0') threw parse_error.106
- the empty token threw out_of_range.404

Both tokens are valid object keys, and both work when the level already
exists as an object, so creating the level changed the outcome. Test the
token against the RFC 6901, Sect. 4 grammar for array indices instead, so
that such tokens create an object. This only affects pointers that threw
before.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Clarify that the token 0 is a valid array index in operator[] docs

The note described valid array indices as "a nonempty sequence of digits
without a leading 0", which read literally excludes the token "0" itself,
contradicting both the code and the /foo/0/0/0 example in the next
sentence. Spell out the RFC 6901 grammar instead.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 17:13:48 +01:00
Niels LohmannandClaude Sonnet 5.5 c712df0637 docs: make the diff() and patch() documentation accurate; add tests (#5939)
* Throw out_of_range.411 for JSON Patch add into a null parent (#5917)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKJf48vnGtTE25KscmjfaW
Signed-off-by: Niels Lohmann <niels.lohmann@gmail.com>

* docs: make the diff() and patch() documentation accurate; add tests

Document the exceptions to the diff round-trip invariant (NaN, discarded
values, number types), the path parameter, non-minimal patches, array and
object operation order, the ordered_json fallback, binary values in
serialized patches, the real complexity, duplicate member names in
operations, test on missing paths, and the exact out_of_range.403 message.
Add tests for these behaviors.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

---------

Signed-off-by: Niels Lohmann <niels.lohmann@gmail.com>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 17:12:05 +01:00
Niels Lohmann 7a10bf25d2 docs: state merge_patch complexity and RFC 7396 limitations; test non-object, discarded, binary cases (#5938)
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 17:11:55 +01:00
Niels Lohmann c20b6eddf4 docs: correct JSON Pointer documentation and add tests (#5937)
Fix inaccurate or missing statements about unflatten (third round-trip
failure, empty token, typo), value (type_error.306 before pointer lookup),
contains (pointer overload), parent_pointer and operator<< (bad_alloc),
out_of_range.410 example, template parameter requirements (resize(n)), and
document the URI fragment form and non-unique member names of RFC 6901.
Add tests for the unflatten and value() cases.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 17:11:46 +01:00
Niels Lohmann 8679c011db Use flawfinder --exclude instead of listing directories (#5935)
Flawfinder 2.0.20 has an --exclude PATTERN option. Scan `./` and exclude
`*/single_include/*` so that new top-level directories are covered
automatically. The scanned file set is identical to the explicit
directory list.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 17:11:36 +01:00
Niels LohmannandClaude Sonnet 5.5 d6431a9104 Throw out_of_range.411 for JSON Patch add into a null parent (#5917) (#5918)
Claude-Session: https://claude.ai/code/session_01MKJf48vnGtTE25KscmjfaW

Signed-off-by: Niels Lohmann <niels.lohmann@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 17:11:25 +01:00
26 changed files with 1712 additions and 87 deletions

No files matched your search

+2 -2
View File
@@ -43,8 +43,8 @@ jobs:
- name: flawfinder_scan - name: flawfinder_scan
uses: david-a-wheeler/flawfinder@c4216b74cf2639ffa98503768bd6e4299b5440c9 # v2.0.20 uses: david-a-wheeler/flawfinder@c4216b74cf2639ffa98503768bd6e4299b5440c9 # v2.0.20
with: with:
# single_include/ is omitted: it is generated from include/ and would report every finding twice # single_include/ is generated from include/ and would report every finding twice
arguments: '--sarif ./cmake ./docs ./include ./src ./tests ./tools' arguments: "--sarif --exclude '*/single_include/*' ./"
output: 'flawfinder_results.sarif' output: 'flawfinder_results.sarif'
- name: Upload analysis results to GitHub Security tab - name: Upload analysis results to GitHub Security tab
+2 -1
View File
@@ -90,7 +90,8 @@ Strong exception safety: if an exception occurs, the original value stays intact
1. Constant. 1. Constant.
2. Logarithmic in the size of the container. 2. Logarithmic in the size of the container.
3. Logarithmic in the size of the container. 3. Logarithmic in the size of the container.
4. Logarithmic in the size of the container. 4. Linear in the number of reference tokens of `ptr`; for each of them, logarithmic in the size of the object, or
constant for an array.
## Notes ## Notes
+9 -2
View File
@@ -61,11 +61,18 @@ Strong exception safety: if an exception occurs, the original value stays intact
## Complexity ## Complexity
Logarithmic in the size of the JSON object. 1. Logarithmic in the size of the JSON object.
2. Logarithmic in the size of the JSON object.
3. At most one lookup per reference token of `ptr`, each logarithmic in the size of the container at that level:
O(*n* log *m*) for *n* reference tokens and containers of size at most *m*.
4. Deleted; a call with an integral argument does not compile.
## Notes ## Notes
- This method always returns `#!cpp false` when executed on a JSON type that is not an object. - Overloads (1) and (2) always return `#!cpp false` when executed on a JSON type that is not an object. Overload (3)
also works on arrays and on primitive values: the empty JSON pointer resolves in every JSON value (so
`#!cpp json(1).contains(""_json_pointer)` returns `#!cpp true`), and `#!cpp json({1, 2}).contains("/1"_json_pointer)`
returns `#!cpp true`.
- This method can be executed on any JSON value type. - This method can be executed on any JSON value type.
- Calling this function with an integer argument (for example, `#!cpp contains(0)`) does not compile: such an argument - Calling this function with an integer argument (for example, `#!cpp contains(0)`) does not compile: such an argument
would otherwise implicitly convert to a null `#!cpp const char*` and, from there, cause undefined behavior when would otherwise implicitly convert to a null `#!cpp const char*` and, from there, cause undefined behavior when
+41 -4
View File
@@ -2,17 +2,27 @@
```cpp ```cpp
static basic_json diff(const basic_json& source, static basic_json diff(const basic_json& source,
const basic_json& target); const basic_json& target,
const string_t& path = "");
``` ```
Creates a [JSON Patch](http://jsonpatch.com) so that value `source` can be changed into the value `target` by calling Creates a [JSON Patch](http://jsonpatch.com) so that value `source` can be changed into the value `target` by calling
[`patch`](patch.md) function. [`patch`](patch.md) function.
For two JSON values `source` and `target`, the following code always yields `#!cpp true`: For two JSON values `source` and `target`, the following code yields `#!cpp true`:
```cpp ```cpp
source.patch(diff(source, target)) == target; source.patch(diff(source, target)) == target;
``` ```
The equality holds up to [`operator==`](operator_eq.md), so it has the following exceptions:
- A value that is not equal to itself cannot round-trip. This applies to `NaN` (also nested in an array or object) and
to [discarded](is_discarded.md) values: `diff` emits a `replace` operation for them. A `NaN` in the patch is
serialized as `#!json null`, so a patch that was dumped and parsed again would set `#!json null`.
- Numbers that compare equal produce no operation, even if their types or signs differ: `diff(1, 1.0)`,
`diff(1u, 1)` (unsigned and signed integer) and `diff(0.0, -0.0)` all return `#!json []`. The patched value is then
the `source` and keeps its number type.
## Parameters ## Parameters
`source` (in) `source` (in)
@@ -21,6 +31,12 @@ source.patch(diff(source, target)) == target;
`target` (in) `target` (in)
: JSON value to compare against : JSON value to compare against
`path` (in)
: a prefix that is put verbatim in front of the `path` member of every operation (default: `""`, which addresses the
root of the value). The prefix is neither escaped nor validated: `#!cpp diff(1, 2, "abc")` yields an operation with
the `path` `abc`, which is not a valid JSON Pointer. Use it with a valid JSON Pointer such as `/root` and escape
`~` and `/` in its reference tokens (see [JSON Pointer](../../features/json_pointer.md)).
## Return value ## Return value
a JSON patch to convert the `source` to `target` a JSON patch to convert the `source` to `target`
@@ -31,11 +47,32 @@ Strong guarantee: `source` and `target` are never modified.
## Complexity ## Complexity
Linear in the lengths of `source` and `target`. Linear in the total number of values (including nested ones) in `source` and `target`, times the cost of a key-based
object operation (looking up a key in an object): logarithmic for [`json`](../json.md), and linear for
[`ordered_json`](../ordered_json.md) (see [`ordered_map` complexity](../ordered_map.md#complexity)). For
`ordered_json`, diffing two objects with `n` and `m` members therefore takes O((n+m)²) when their keys do not line up
(e.g., a key was added or the keys are in a different order). The size of the patch is proportional to the number of
operations times the length of their paths, so a small change deep inside a value yields a long `path`.
## Notes ## Notes
Currently, only `remove`, `add`, and `replace` operations are generated. - Currently, only `remove`, `add`, and `replace` operations are generated.
- The patch is correct, but not guaranteed to be minimal. For instance, `diff([1,2,3], [0,1,2,3])` yields three
`replace` operations and one `add` operation instead of a single insertion.
- Differences between two arrays are reported index by index. Elements that were added at the end of the array are added
with `add` operations on the path `/-` (appended in order), and elements that were removed are removed with `remove`
operations from the highest index down to the lowest, so that the remaining indices stay valid.
- For objects, the operations for the members of `source` come first (`replace`, `remove`, or the operations for nested
values, in the iteration order of `source`), followed by `add` operations for the members that only `target` has.
- For [`ordered_json`](../ordered_json.md), objects with the same keys in a different order are different. As a patch
cannot reorder members, `diff` removes all keys of the `source` object and adds them again in the order of the
`target`.
- If `source` and `target` differ as a whole (e.g., different types), the patch consists of a single `replace`
operation for the root of the value, i.e., with the `path` `""` (or the given `path` prefix).
- The `value` members of the patch hold copies of values of `target`. A [binary](../../features/binary_values.md) value
has no JSON representation: when a patch that contains one is serialized, it becomes an object such as
`#!json {"bytes":[1,3],"subtype":null}`, so a parsed patch no longer holds a binary value (and would set that object
instead).
## Examples ## Examples
+23 -3
View File
@@ -39,13 +39,33 @@ Basic guarantee: if an exception is thrown during the operation, the JSON value
## Complexity ## Complexity
Linear in the lengths of `apply_patch`. Linear in the total number of values (including nested ones) in `apply_patch`, times the cost of a key-based object
operation (looking up, adding, or erasing a member): logarithmic for [`json`](../json.md), and linear for
[`ordered_json`](../ordered_json.md) (see [`ordered_map` complexity](../ordered_map.md#complexity)). For
`ordered_json`, patching an object that has `n` members with a patch that has `m` members therefore takes O(n·m). The
cost also includes one copy of `apply_patch` (see the notes below).
## Notes ## Notes
`apply_patch` may be `#!cpp *this` itself or refer to a value contained in `#!cpp *this` (for example, a subobject - `apply_patch` may be `#!cpp *this` itself or refer to a value contained in `#!cpp *this` (for example, a subobject
returned by `#!cpp (*this)[key]`); it is read as it was when `merge_patch()` was called, before any modification of returned by `#!cpp (*this)[key]`); it is read as it was when `merge_patch()` was called, before any modification of
`#!cpp *this`. `#!cpp *this`. To make this possible, the function copies `apply_patch` before it modifies `#!cpp *this`, so the cost
includes one copy of the patch.
- A merge patch uses `#!json null` to remove a member, so it cannot set a member to `#!json null`: applying
`#!json {"a":null}` to `#!json {"a":1}` gives `#!json {}`.
- Arrays are not merged: an array in the patch replaces the target value as a whole, and a `#!json null` inside an
array is kept as an element. `#!json null` members of the target that the patch does not mention are preserved.
- A `#!json null` in the patch only removes a member; it does not prevent the creation of the enclosing objects.
Applying `#!json {"a":{"b":null}}` to `#!json {}` gives `#!json {"a":{}}` (see example 15 in Appendix A of the RFC).
- If `apply_patch` is not an object, it replaces the target as a whole. In particular, a `#!json null` patch turns the
target into `#!json null`. If `apply_patch` is an object and the target is not, the target is first replaced by an
empty object, so applying `#!json {}` to `#!json 1` gives `#!json {}`.
- A merge patch cannot express every change; for example, it cannot reorder array elements or remove a specific array
element. Use [JSON Patch](../../features/json_patch.md) ([RFC 6902](https://tools.ietf.org/html/rfc6902)) for that.
[`merge_diff`](merge_diff.md) has a matching limitation, see its notes.
- A [discarded](is_discarded.md) value is treated like any other non-object value: a discarded
`apply_patch` turns the target into a discarded value, and an object patch replaces a discarded target by an object
as described above.
## Examples ## Examples
+27 -6
View File
@@ -92,12 +92,20 @@ Strong exception safety: if an exception occurs, the original value stays intact
For the **const** version, an object key or array index in `ptr` that does not exist is not reported by an For the **const** version, an object key or array index in `ptr` that does not exist is not reported by an
exception, but is undefined behavior (see the notes below). Use [`at`](at.md) for checked access. exception, but is undefined behavior (see the notes below). Use [`at`](at.md) for checked access.
If the **non-const** version throws, the value is left unchanged: values it created for earlier reference tokens
of `ptr` are removed again. This also holds for exceptions such as `#!cpp std::bad_alloc` or
`#!cpp std::length_error` thrown while padding an array up to a very large index. With custom
[template parameters](../../features/types/template_parameters.md), this requires that `erase()` of the object and
array types and the move operations of the custom base class do not throw.
## Complexity ## Complexity
1. Constant if `idx` is in the range of the array. Otherwise, linear in `idx - size()`. 1. Constant if `idx` is in the range of the array. Otherwise, linear in `idx - size()`.
2. Logarithmic in the size of the container. 2. Logarithmic in the size of the container.
3. Logarithmic in the size of the container. 3. Logarithmic in the size of the container.
4. Logarithmic in the size of the container. 4. Linear in the number of reference tokens of `ptr`; for each of them, logarithmic in the size of the object, or
constant for an array. When the non-const version pads an array with `#!json null` values, it is additionally
linear in `idx - size()` for that array.
## Notes ## Notes
@@ -137,10 +145,21 @@ Strong exception safety: if an exception occurs, the original value stays intact
When the JSON pointer traverses intermediate levels that don't exist at all yet (not just a missing When the JSON pointer traverses intermediate levels that don't exist at all yet (not just a missing
leaf), each missing level is created as an array or an object depending on whether the corresponding leaf), each missing level is created as an array or an object depending on whether the corresponding
pointer token parses as a non-negative integer: a numeric token creates an array, a non-numeric token pointer token is a valid array index: the token `0`, a sequence of digits that does not begin with `0`,
creates an object. For example, on an initially `#!json null` value, `/foo/0/0/0` creates nested arrays, or the token `-` creates an array, and every other token creates an object. For example, on an
while `/foo/one/one/one` creates nested objects. This is not specified by the JSON Pointer RFC; it is initially `#!json null` value, `/foo/0/0/0` creates nested arrays, while `/foo/one/one/one` creates
this library's own, intentional disambiguation rule. See also [JSON Pointer](../../features/json_pointer.md). nested objects. Tokens such as `01` or the empty token cannot be array indices (cf. RFC 6901, Sect. 4)
and therefore create objects, just as they would if the level already existed as an object. This is not
specified by the JSON Pointer RFC; it is this library's own, intentional disambiguation rule. See also
[JSON Pointer](../../features/json_pointer.md).
!!! warning "Large array indices"
As the array is filled up with `#!json null` values to the given index, the memory used grows with the index
and not with the size of the pointer: on a 64-bit platform, the index `10000000` allocates about 160 MB.
The library does not limit the index, so check the array indices of a JSON pointer from untrusted input
before using it with the non-const version. See
[Parsing untrusted input](../../features/parsing/untrusted_input.md#json-pointers-and-flattened-keys).
!!! warning "Deprecation" !!! warning "Deprecation"
@@ -291,4 +310,6 @@ Strong exception safety: if an exception occurs, the original value stays intact
4. Added in version 2.0.0. A missing array index in the const version is guarded by a runtime assertion since 4. Added in version 2.0.0. A missing array index in the const version is guarded by a runtime assertion since
version 3.13.0. Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) instead of version 3.13.0. Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) instead of
[`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) for a one-character array index that is not [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) for a one-character array index that is not
a digit (e.g., `/x`) in version 3.13.0, as it already did for longer ones. a digit (e.g., `/x`) in version 3.13.0, as it already did for longer ones. Fixed in version 3.13.0 to leave the value
unchanged if the non-const version throws (strong exception safety, as documented); before, values created for earlier
reference tokens of `ptr` were kept.
+10 -2
View File
@@ -40,7 +40,7 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va
where an existing element is required (the "path" of "replace", the "from" of "move" and "copy"); example: where an existing element is required (the "path" of "replace", the "from" of "move" and "copy"); example:
`"array index '-' (3) is out of range"`. `"array index '-' (3) is out of range"`.
- Throws [`out_of_range.403`](../../home/exceptions.md#jsonexceptionout_of_range403) if a JSON pointer inside the patch - Throws [`out_of_range.403`](../../home/exceptions.md#jsonexceptionout_of_range403) if a JSON pointer inside the patch
could not be resolved successfully in the current JSON value; example: `"key baz not found"`. could not be resolved successfully in the current JSON value; example: `"key 'baz' not found"`.
- Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if a reference token of a JSON - Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if a reference token of a JSON
pointer inside the patch cannot be resolved, e.g., `-` in a "remove" operation or `1a` for an array; example: pointer inside the patch cannot be resolved, e.g., `-` in a "remove" operation or `1a` for an array; example:
`"unresolved reference token '-'"`. `"unresolved reference token '-'"`.
@@ -53,7 +53,10 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va
- Throws [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) if a "move" operation's "from" - Throws [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) if a "move" operation's "from"
location is a proper prefix of its "path" location. location is a proper prefix of its "path" location.
- Throws [`other_error.501`](../../home/exceptions.md#jsonexceptionother_error501) if "test" operation was - Throws [`other_error.501`](../../home/exceptions.md#jsonexceptionother_error501) if "test" operation was
unsuccessful. unsuccessful. A "test" operation whose "path" does not exist is unsuccessful, too: the failed lookup is reported as a
failed test, even if the operation has no "value" member (which would otherwise be
[`parse_error.105`](../../home/exceptions.md#jsonexceptionparse_error105)). Which of the two errors is reported for
this combination depends on the order in which the operands of a comparison are evaluated, so do not rely on it.
## Complexity ## Complexity
@@ -65,6 +68,10 @@ the JSON value, the complexity can usually be neglected.
The application of a patch is atomic: Either all operations succeed and the patched document is returned or an exception The application of a patch is atomic: Either all operations succeed and the patched document is returned or an exception
is thrown. In any case, the original value is not changed: the patch is applied to a copy of the value. is thrown. In any case, the original value is not changed: the patch is applied to a copy of the value.
Duplicate member names inside an operation object are resolved by the parser before the patch is applied (the last one
is kept, see [`object_t`](object_t.md#behavior)), so `#!json {"op": "add", "op": "remove", "path": "/a"}` is a
`remove` operation.
## Examples ## Examples
??? example "Example: apply a JSON patch" ??? example "Example: apply a JSON patch"
@@ -108,6 +115,7 @@ is thrown. In any case, the original value is not changed: the patch is applied
- Added in version 2.0.0. - Added in version 2.0.0.
- Added [`out_of_range.411`](../../home/exceptions.md#jsonexceptionout_of_range411) and stopped relying on an internal assertion when an "add" operation's - Added [`out_of_range.411`](../../home/exceptions.md#jsonexceptionout_of_range411) and stopped relying on an internal assertion when an "add" operation's
target location has a non-object/non-array parent in version 3.13.0. target location has a non-object/non-array parent in version 3.13.0.
This includes a `null` parent, which was previously silently converted to an object, in version 3.13.0.
- Added [`out_of_range.413`](../../home/exceptions.md#jsonexceptionout_of_range413) and stopped silently ignoring a "remove" operation whose target - Added [`out_of_range.413`](../../home/exceptions.md#jsonexceptionout_of_range413) and stopped silently ignoring a "remove" operation whose target
location has a non-object/non-array parent in version 3.13.0. location has a non-object/non-array parent in version 3.13.0.
- Added [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) and rejected a "move" operation whose "from" location is a proper - Added [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) and rejected a "move" operation whose "from" location is a proper
@@ -36,7 +36,7 @@ No guarantees, value may be corrupted by an unsuccessful patch operation.
where an existing element is required (the "path" of "replace", the "from" of "move" and "copy"); example: where an existing element is required (the "path" of "replace", the "from" of "move" and "copy"); example:
`"array index '-' (3) is out of range"`. `"array index '-' (3) is out of range"`.
- Throws [`out_of_range.403`](../../home/exceptions.md#jsonexceptionout_of_range403) if a JSON pointer inside the patch - Throws [`out_of_range.403`](../../home/exceptions.md#jsonexceptionout_of_range403) if a JSON pointer inside the patch
could not be resolved successfully in the current JSON value; example: `"key baz not found"`. could not be resolved successfully in the current JSON value; example: `"key 'baz' not found"`.
- Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if a reference token of a JSON - Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if a reference token of a JSON
pointer inside the patch cannot be resolved, e.g., `-` in a "remove" operation or `1a` for an array; example: pointer inside the patch cannot be resolved, e.g., `-` in a "remove" operation or `1a` for an array; example:
`"unresolved reference token '-'"`. `"unresolved reference token '-'"`.
@@ -49,7 +49,10 @@ No guarantees, value may be corrupted by an unsuccessful patch operation.
- Throws [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) if a "move" operation's "from" - Throws [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) if a "move" operation's "from"
location is a proper prefix of its "path" location. location is a proper prefix of its "path" location.
- Throws [`other_error.501`](../../home/exceptions.md#jsonexceptionother_error501) if "test" operation was - Throws [`other_error.501`](../../home/exceptions.md#jsonexceptionother_error501) if "test" operation was
unsuccessful. unsuccessful. A "test" operation whose "path" does not exist is unsuccessful, too: the failed lookup is reported as a
failed test, even if the operation has no "value" member (which would otherwise be
[`parse_error.105`](../../home/exceptions.md#jsonexceptionparse_error105)). Which of the two errors is reported for
this combination depends on the order in which the operands of a comparison are evaluated, so do not rely on it.
## Complexity ## Complexity
@@ -62,6 +65,10 @@ Unlike [`patch`](patch.md), `patch_inplace` applies the operation "in place" and
That makes it faster for large documents by avoiding the copy. However, the JSON value might be corrupted if the That makes it faster for large documents by avoiding the copy. However, the JSON value might be corrupted if the
function throws an exception. function throws an exception.
Duplicate member names inside an operation object are resolved by the parser before the patch is applied (the last one
is kept, see [`object_t`](object_t.md#behavior)), so `#!json {"op": "add", "op": "remove", "path": "/a"}` is a
`remove` operation.
## Examples ## Examples
??? example "Example: apply a JSON patch in place" ??? example "Example: apply a JSON patch in place"
@@ -106,6 +113,7 @@ function throws an exception.
- Added in version 3.11.0. - Added in version 3.11.0.
- Added [`out_of_range.411`](../../home/exceptions.md#jsonexceptionout_of_range411) and stopped relying on an internal assertion when an "add" operation's - Added [`out_of_range.411`](../../home/exceptions.md#jsonexceptionout_of_range411) and stopped relying on an internal assertion when an "add" operation's
target location has a non-object/non-array parent in version 3.13.0. target location has a non-object/non-array parent in version 3.13.0.
This includes a `null` parent, which was previously silently converted to an object, in version 3.13.0.
- Added [`out_of_range.413`](../../home/exceptions.md#jsonexceptionout_of_range413) and stopped silently ignoring a "remove" operation whose target - Added [`out_of_range.413`](../../home/exceptions.md#jsonexceptionout_of_range413) and stopped silently ignoring a "remove" operation whose target
location has a non-object/non-array parent in version 3.13.0. location has a non-object/non-array parent in version 3.13.0.
- Added [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) and rejected a "move" operation whose "from" location is a proper - Added [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) and rejected a "move" operation whose "from" location is a proper
+10 -5
View File
@@ -36,9 +36,9 @@ The function can throw the following exceptions:
- Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) if an array index in a key is not a - Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) if an array index in a key is not a
number; example: `"array index 'one' is not a number"` number; example: `"array index 'one' is not a number"`
- Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if a level becomes an array - Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if a level becomes an array
(because one of its keys is `0`) and another key at that level begins with a digit but is not a valid array index (because one of its keys is `0`) and another key at that level is empty (as in `/a/`), is `-`, or begins with a digit
(such as `1a`), or is `-`; example: but is not a valid array index (such as `1a`); examples: `"unresolved reference token '-'"`,
`"unresolved reference token '-'"` `"unresolved reference token ''"`
## Complexity ## Complexity
@@ -46,7 +46,7 @@ Linear in the size of the JSON value.
## Notes ## Notes
Empty objects and arrays are flattened by [`flatten()`](flatten.md) to `#!json null` values and cannot unflattened to Empty objects and arrays are flattened by [`flatten()`](flatten.md) to `#!json null` values and cannot be unflattened to
their original type. their original type.
A flattened array and a flattened object whose keys are array indices are indistinguishable, because both are A flattened array and a flattened object whose keys are array indices are indistinguishable, because both are
@@ -54,7 +54,12 @@ described by the same JSON pointers. A value is therefore restored as an array i
reference token `0`, and as an object otherwise: `#!json {"2": 1}` is restored unchanged, whereas `#!json {"0": 1}` is reference token `0`, and as an object otherwise: `#!json {"2": 1}` is restored unchanged, whereas `#!json {"0": 1}` is
restored as `#!json [1]`. This decision does not depend on the order in which the flattened object is iterated. restored as `#!json [1]`. This decision does not depend on the order in which the flattened object is iterated.
Apart from these two cases, for a JSON value `j`, the following is always true: A third case is an object that has the key `0` next to a key that is not a valid array index: `#!json {"0": 1, "b": 2}`
is flattened to `#!json {"/0": 1, "/b": 2}`, and unflatten treats the level as an array and throws
[`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) because `b` is not a number. (For the keys
`-`, `1a`, or the empty key instead of `b`, it throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404).)
Apart from these three cases, for a JSON value `j`, the following is always true:
`#!cpp j == j.flatten().unflatten()`. `#!cpp j == j.flatten().unflatten()`.
## Examples ## Examples
+3
View File
@@ -45,6 +45,9 @@ This is equivalent to Python's `dict.get(key, default)`.
return default_value; return default_value;
} }
``` ```
with one exception: if the JSON value is neither an array nor an object, the function throws
[`type_error.306`](../../home/exceptions.md#jsonexceptiontype_error306) before looking at `ptr`, even for the empty
pointer, whereas `at` returns the value itself for the empty pointer.
!!! note "Differences to `at` and `operator[]`" !!! note "Differences to `at` and `operator[]`"
@@ -12,7 +12,8 @@ Parent of this JSON pointer; in case this JSON pointer is the root, the root its
## Exception safety ## Exception safety
No-throw guarantee: this function never throws exceptions. Strong exception safety: if an exception occurs, the original value stays intact. The function is not `#!cpp noexcept`
and allocates a new JSON pointer, so it can throw `#!cpp std::bad_alloc`; it throws no library exceptions.
## Complexity ## Complexity
+2 -1
View File
@@ -37,7 +37,8 @@ the stream `o`
1. Throws [`type_error.316`](../home/exceptions.md#jsonexceptiontype_error316) if a string stored inside the JSON 1. Throws [`type_error.316`](../home/exceptions.md#jsonexceptiontype_error316) if a string stored inside the JSON
value is not UTF-8 encoded. Note that unlike the [`dump`](basic_json/dump.md) member functions, no `error_handler` value is not UTF-8 encoded. Note that unlike the [`dump`](basic_json/dump.md) member functions, no `error_handler`
can be set. can be set.
2. None. 2. Throws no library exceptions. As the string representation is built with [`to_string`](json_pointer/to_string.md),
`#!cpp std::bad_alloc` is possible.
## Complexity ## Complexity
+18 -1
View File
@@ -13,6 +13,12 @@ to the current JSON value by executing all operations from the patch, yielding t
If the document is large and the original value is no longer needed, If the document is large and the original value is no longer needed,
[`patch_inplace`](../api/basic_json/patch_inplace.md) applies the same operations in place instead. [`patch_inplace`](../api/basic_json/patch_inplace.md) applies the same operations in place instead.
!!! note "Duplicate member names"
Duplicate member names inside an operation object are resolved by the parser before the patch is applied (the last
one is kept, see [`object_t`](../api/basic_json/object_t.md#behavior)), so
`#!json {"op": "add", "op": "remove", "path": "/a"}` is a `remove` operation.
??? example "Example: apply a JSON Patch" ??? example "Example: apply a JSON Patch"
The following code shows how a JSON patch is applied to a value. The following code shows how a JSON patch is applied to a value.
@@ -41,12 +47,23 @@ flowchart LR
!!! success "Invariant" !!! success "Invariant"
For two JSON values *source* and *target*, the following code yields always true: For two JSON values *source* and *target*, the following code yields true:
```cpp ```cpp
source.patch(diff(source, target)) == target; source.patch(diff(source, target)) == target;
``` ```
The equality holds up to [`operator==`](../api/basic_json/operator_eq.md), with these exceptions:
- `NaN` and [discarded](../api/basic_json/is_discarded.md) values are not equal to themselves, so the invariant
cannot hold for them. `diff` emits a `replace` operation for such a value, and a `NaN` in a patch is serialized
as `#!json null`.
- Numbers that compare equal produce no operation even if their types differ: `diff(1, 1.0)`, `diff(1u, 1)`, and
`diff(0.0, -0.0)` return `#!json []`, so the patched value keeps the number type of *source*.
- A [binary value](binary_values.md) in a patch is serialized as an object
(`#!json {"bytes":[...],"subtype":null}`), so a patch that was dumped and parsed again no longer holds a binary
value.
??? example "Example: create a JSON Patch from the difference of two values" ??? example "Example: create a JSON Patch from the difference of two values"
The following code shows how a JSON patch is created as a diff for two JSON values. The following code shows how a JSON patch is created as a diff for two JSON values.
+11
View File
@@ -37,6 +37,10 @@ Then every value inside the JSON document can be identified as follows:
Note `/` does not identify the root (i.e., the whole document), but an object entry with empty key `""`. See Note `/` does not identify the root (i.e., the whole document), but an object entry with empty key `""`. See
[RFC 6901](https://tools.ietf.org/html/rfc6901) for more information. [RFC 6901](https://tools.ietf.org/html/rfc6901) for more information.
[RFC 6901, section 4](https://tools.ietf.org/html/rfc6901#section-4) states that evaluation fails if an object has
non-unique member names. A `basic_json` object holds one value per key (see [`object_t`](../api/basic_json/object_t.md)
for how duplicate keys are handled while parsing), so evaluating a JSON Pointer never encounters duplicates.
## JSON Pointer creation ## JSON Pointer creation
JSON Pointers can be created from a string: JSON Pointers can be created from a string:
@@ -54,6 +58,13 @@ auto p = "/nested/one"_json_pointer;
The escaping rules of [RFC 6901](https://tools.ietf.org/html/rfc6901) are implemented. See the The escaping rules of [RFC 6901](https://tools.ietf.org/html/rfc6901) are implemented. See the
[constructor documentation](../api/json_pointer/json_pointer.md) for more information. [constructor documentation](../api/json_pointer/json_pointer.md) for more information.
!!! note "URI fragment representation"
[RFC 6901, section 6](https://tools.ietf.org/html/rfc6901#section-6) also defines a URI fragment identifier
representation (`#/a%20b`). It is not supported: `#!cpp json::json_pointer("#/a")` throws
[`parse_error.107`](../home/exceptions.md#jsonexceptionparse_error107). Strip the leading `#` and
percent-decode the remainder before creating the JSON pointer.
## Value access ## Value access
JSON Pointers can be used in the [`at`](../api/basic_json/at.md), [`operator[]`](../api/basic_json/operator%5B%5D.md), JSON Pointers can be used in the [`at`](../api/basic_json/at.md), [`operator[]`](../api/basic_json/operator%5B%5D.md),
+13 -1
View File
@@ -2,7 +2,7 @@
The library supports JSON Merge Patch ([RFC 7396](https://tools.ietf.org/html/rfc7396)) as a patch format. The library supports JSON Merge Patch ([RFC 7396](https://tools.ietf.org/html/rfc7396)) as a patch format.
The merge patch format is primarily intended for use with the HTTP PATCH method as a means of describing a set of The merge patch format is primarily intended for use with the HTTP PATCH method as a means of describing a set of
modifications to a target resource's content. This function applies a merge patch to the current JSON value. modifications to a target resource's content. Applying a merge patch changes the JSON value the patch is applied to.
Instead of using [JSON Pointer](json_pointer.md) to specify values to be manipulated, it describes the changes using a Instead of using [JSON Pointer](json_pointer.md) to specify values to be manipulated, it describes the changes using a
syntax that closely mimics the document being modified. Unlike [JSON Patch](json_patch.md), a JSON Merge Patch cannot syntax that closely mimics the document being modified. Unlike [JSON Patch](json_patch.md), a JSON Merge Patch cannot
@@ -19,6 +19,18 @@ another.
[`update`](../api/basic_json/update.md) with `merge_objects` set to `#!cpp true`; see [`update`](../api/basic_json/update.md) with `merge_objects` set to `#!cpp true`; see
[Merging objects](modifying_values.md#merging-objects). [Merging objects](modifying_values.md#merging-objects).
!!! note "Limitations of RFC 7396"
- A patch cannot set a member to `#!json null`, because `#!json null` removes the member:
`#!json {"a":1}` patched with `#!json {"a":null}` gives `#!json {}`.
- Arrays are replaced as a whole, never merged, and a `#!json null` inside an array is kept.
- `#!json null` members of the target that the patch does not mention are preserved.
- A `#!json null` that would remove a member of a not yet existing object still creates that object:
`#!json {}` patched with `#!json {"a":{"b":null}}` gives `#!json {"a":{}}`.
- A patch that is not an object replaces the whole target (a `#!json null` patch makes the target
`#!json null`); an object patch applied to a non-object target starts from an empty object.
- [`merge_diff`](../api/basic_json/merge_diff.md#notes) has a matching limitation for `#!json null` members.
??? example ??? example
The following code shows how a JSON Merge Patch is applied to a JSON document. The following code shows how a JSON Merge Patch is applied to a JSON document.
@@ -130,6 +130,17 @@ parameters of [`parse()`](../../api/basic_json/parse.md) or [`accept()`](../../a
enable either for input whose conformance you cannot otherwise control, since interoperability with strictly enable either for input whose conformance you cannot otherwise control, since interoperability with strictly
conforming JSON consumers is exactly what the default rejects. conforming JSON consumers is exactly what the default rejects.
## JSON Pointers and flattened keys
A [JSON Pointer](../json_pointer.md) can come from untrusted input as well, for instance as a path in a request or as a
key of a flattened object. The non-const [`operator[]`](../../api/basic_json/operator%5B%5D.md) with a JSON Pointer and
[`unflatten`](../../api/basic_json/unflatten.md) create missing values, and when an array index lies beyond the end of
an array, they fill the array with `#!json null` values up to that index. The memory used therefore grows with the
index, not with the size of the pointer: on a 64-bit platform, the index `10000000` allocates about 160 MB, the index
`100000000` about 1.6 GB. The library does not limit the index. For pointers from untrusted input, check the array
indices (or the pointer as a whole) before using them, or use [`at`](../../api/basic_json/at.md) or
[`contains`](../../api/basic_json/contains.md), which never add values.
## Checklist ## Checklist
- Wrap parsing in a `#!cpp try`/`#!cpp catch` block, or use `allow_exceptions=false`/`accept()` if your environment - Wrap parsing in a `#!cpp try`/`#!cpp catch` block, or use `allow_exceptions=false`/`accept()` if your environment
@@ -145,6 +156,8 @@ conforming JSON consumers is exactly what the default rejects.
`JSON_STRICT_NUL_HANDLING`. `JSON_STRICT_NUL_HANDLING`.
- Decide whether duplicate object keys should be an error for your application, and add a callback if so. - Decide whether duplicate object keys should be an error for your application, and add a callback if so.
- Do not enable `ignore_comments` or `ignore_trailing_commas` for input that must be strictly conforming JSON. - Do not enable `ignore_comments` or `ignore_trailing_commas` for input that must be strictly conforming JSON.
- Check the array indices of untrusted JSON Pointers and flattened keys before passing them to the non-const
`operator[]` or `unflatten()`, which fill arrays with `#!json null` values up to the index.
For the broader design rationale and how it is tested (fuzzing, sanitizers, static analysis), see the For the broader design rationale and how it is tested (fuzzing, sanitizers, static analysis), see the
[assurance case](../../community/assurance_case.md) and [quality assurance](../../community/quality_assurance.md). To [assurance case](../../community/assurance_case.md) and [quality assurance](../../community/quality_assurance.md). To
@@ -390,6 +390,7 @@ using array_t = ArrayType<basic_json, AllocatorType<basic_json>>;
described in RFC 6901. Neither `find(const StringType&, size_type)`, nor `substr(pos, count)`, nor described in RFC 6901. Neither `find(const StringType&, size_type)`, nor `substr(pos, count)`, nor
`replace(pos, count, const StringType&)` is required. `replace(pos, count, const StringType&)` is required.
- `empty()`. - `empty()`.
- `resize(n)` (one argument) -- used by `to_string` to size the result before the reference tokens are copied into it.
- `begin()` and `end()` -- used by - `begin()` and `end()` -- used by
[`operator[](const json_pointer&)`](../../api/basic_json/operator%5B%5D.md) to decide whether a reference token [`operator[](const json_pointer&)`](../../api/basic_json/operator%5B%5D.md) to decide whether a reference token
denotes an array index. denotes an array index.
+8 -1
View File
@@ -1083,9 +1083,14 @@ a JSON pointer exceeds the range of `size_type` (e.g., on 32-bit platforms).
``` ```
enum value out of range enum value out of range
array index 18446744073709551616 exceeds size_type array index 18446744073709551615 exceeds size_type
``` ```
The second message is thrown, for instance, by `#!cpp json::parse("[1, 2]").at("/18446744073709551615"_json_pointer)`
on a 64-bit platform, where `size_type` is `#!cpp std::size_t` and this index is `SIZE_MAX`. An index that does not
fit into 64 bits (such as `18446744073709551616`) is not reported as 410 but as
[`out_of_range.404`](#jsonexceptionout_of_range404).
### json.exception.out_of_range.411 ### json.exception.out_of_range.411
A JSON Patch `add` operation cannot be applied because the target location's parent is neither an object nor an array. Per [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902), an `add` target must reference a member of an existing object or an element of an existing array; a primitive value (string, number, boolean, etc.) cannot receive a new member or element. A JSON Patch `add` operation cannot be applied because the target location's parent is neither an object nor an array. Per [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902), an `add` target must reference a member of an existing object or an element of an existing array; a primitive value (string, number, boolean, etc.) cannot receive a new member or element.
@@ -1100,6 +1105,8 @@ A JSON Patch `add` operation cannot be applied because the target location's par
This exception was added in version 3.13.0. Before that, this situation hit an internal assertion (aborting the program in debug builds) or was silently ignored when assertions were disabled. This exception was added in version 3.13.0. Before that, this situation hit an internal assertion (aborting the program in debug builds) or was silently ignored when assertions were disabled.
A `null` parent is rejected as well. Before version 3.13.0, an `add` into a `null` parent silently converted it to an object (e.g., `add` at `/foo/bar` turned `{"foo":null}` into `{"foo":{"bar":1}}`).
### json.exception.out_of_range.412 ### json.exception.out_of_range.412
BSON stores the length of documents, arrays, strings, and binary values in a signed 32-bit integer, and MessagePack BSON stores the length of documents, arrays, strings, and binary values in a signed 32-bit integer, and MessagePack
+488 -26
View File
@@ -8,7 +8,7 @@
#pragma once #pragma once
#include <algorithm> // all_of, copy #include <algorithm> // copy
#include <array> // array #include <array> // array
#include <cctype> // isdigit #include <cctype> // isdigit
#include <cerrno> // errno, ERANGE #include <cerrno> // errno, ERANGE
@@ -16,9 +16,11 @@
#include <cstdint> // uint64_t #include <cstdint> // uint64_t
#include <cstdlib> // strtoull #include <cstdlib> // strtoull
#include <cstring> // memchr, memcmp, memcpy #include <cstring> // memchr, memcmp, memcpy
#include <functional> // less
#ifndef JSON_NO_IO #ifndef JSON_NO_IO
#include <iosfwd> // ostream #include <iosfwd> // ostream
#endif // JSON_NO_IO #endif // JSON_NO_IO
#include <iterator> // next
#include <limits> // max #include <limits> // max
#include <string> // string #include <string> // string
#include <utility> // move #include <utility> // move
@@ -28,6 +30,7 @@
#include <nlohmann/detail/macro_scope.hpp> #include <nlohmann/detail/macro_scope.hpp>
#include <nlohmann/detail/meta/cpp_future.hpp> #include <nlohmann/detail/meta/cpp_future.hpp>
#include <nlohmann/detail/meta/type_traits.hpp> #include <nlohmann/detail/meta/type_traits.hpp>
#include <nlohmann/detail/meta/void_t.hpp>
#include <nlohmann/detail/string_concat.hpp> #include <nlohmann/detail/string_concat.hpp>
#include <nlohmann/detail/string_escape.hpp> #include <nlohmann/detail/string_escape.hpp>
#include <nlohmann/detail/string_utils.hpp> #include <nlohmann/detail/string_utils.hpp>
@@ -134,6 +137,26 @@ JSON_HEDLEY_ALWAYS_INLINE array_index_status parse_array_index(const StringType&
return array_index_status::ok; return array_index_status::ok;
} }
/// whether two keys of type @a StringType are equivalent under @a Compare
/// exactly if they are equal: std::less<StringType> and std::less<>
template<typename Compare, typename StringType>
struct is_less_comparator : std::false_type {};
template<typename StringType>
struct is_less_comparator<std::less<StringType>, StringType> : std::true_type {};
template<typename StringType>
struct is_less_comparator<std::less<void>, StringType> : std::true_type {};
/// whether the objects of type @a ObjectType find a key of type @a StringType
/// exactly if it equals a stored key
template<typename ObjectType, typename StringType, typename = void>
struct finds_equal_keys : std::false_type {};
template<typename ObjectType, typename StringType>
struct finds_equal_keys<ObjectType, StringType, void_t<typename ObjectType::key_compare, typename ObjectType::key_type>>
: conjunction<is_less_comparator<typename ObjectType::key_compare, StringType>, std::is_same<typename ObjectType::key_type, StringType>> {};
} // namespace detail } // namespace detail
/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document /// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document
@@ -589,6 +612,228 @@ class json_pointer
return object_member_impl(object, key, detail::priority_tag<1> {}); return object_member_impl(object, key, detail::priority_tag<1> {});
} }
/// @brief append a null value to @a array, like operator[] with the size of @a array
template<typename BasicJsonType>
static BasicJsonType& array_append(BasicJsonType& array)
{
auto& elements = *array.m_data.m_value.array;
const auto old_capacity = array.container_capacity();
elements.emplace_back();
return array.set_parent_after_insert(elements.back(), old_capacity);
}
/// @brief add @a key, which is missing, to @a object
template<typename BasicJsonType>
static auto object_add_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<1> /*unused*/)
-> decltype(object.m_data.m_value.object->emplace(key, nullptr), std::declval<BasicJsonType&>())
{
const auto old_capacity = object.container_capacity();
const auto it = object.m_data.m_value.object->emplace(key, nullptr).first;
return object.set_parent_after_insert(it->second, old_capacity);
}
template<typename BasicJsonType>
static BasicJsonType& object_add_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<0> /*unused*/)
{
return object.operator[](key);
}
template<typename BasicJsonType>
static BasicJsonType& object_add(BasicJsonType& object, const string_t& key)
{
return object_add_impl(object, key, detail::priority_tag<1> {});
}
/// @brief search @a key in @a members: the result is where the key is or,
/// for objects that can insert at a position (std::map), where it belongs
template<typename ObjectType>
static auto object_search_impl(ObjectType& members, const string_t& key, detail::priority_tag<1> /*unused*/)
-> decltype(members.key_comp()(key, members.lower_bound(key)->first), members.lower_bound(key))
{
return members.lower_bound(key);
}
template<typename ObjectType>
static auto object_search_impl(ObjectType& members, const string_t& key, detail::priority_tag<0> /*unused*/)
-> decltype(members.find(key))
{
return members.find(key);
}
template<typename ObjectType>
static auto object_search(ObjectType& members, const string_t& key)
-> decltype(object_search_impl(members, key, detail::priority_tag<1> {}))
{
return object_search_impl(members, key, detail::priority_tag<1> {});
}
/// @brief whether @a it, the result of @ref object_search for @a key, points to @a key
///
/// With std::less, the key at the lower bound is equivalent to @a key
/// exactly if it is equal to it, and comparing for equality is cheaper.
template<typename ObjectType, typename Iterator,
detail::enable_if_t<detail::finds_equal_keys<ObjectType, string_t>::value, int> = 0>
static auto object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& key, detail::priority_tag<2> /*unused*/)
-> decltype(members.lower_bound(key), it->first == key)
{
return it != members.end() && it->first == key;
}
template<typename ObjectType, typename Iterator>
static auto object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& key, detail::priority_tag<1> /*unused*/)
-> decltype(members.key_comp()(key, it->first), members.lower_bound(key), bool())
{
return it != members.end() && !members.key_comp()(key, it->first);
}
template<typename ObjectType, typename Iterator>
static bool object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& /*key*/, detail::priority_tag<0> /*unused*/)
{
return it != members.end();
}
template<typename ObjectType, typename Iterator>
static bool object_search_found(ObjectType& members, const Iterator& it, const string_t& key)
{
return object_search_found_impl(members, it, key, detail::priority_tag<2> {});
}
/// @brief insert @a key, which is missing, into @a object at @a it, the
/// result of @ref object_search for @a key
template<typename BasicJsonType, typename Iterator>
static auto object_insert_impl(BasicJsonType& object, const Iterator& it, const string_t& key, detail::priority_tag<1> /*unused*/)
-> decltype(object.m_data.m_value.object->emplace_hint(it, key, nullptr), std::declval<BasicJsonType&>())
{
const auto old_capacity = object.container_capacity();
const auto pos = object.m_data.m_value.object->emplace_hint(it, key, nullptr);
return object.set_parent_after_insert(pos->second, old_capacity);
}
template<typename BasicJsonType, typename Iterator>
static BasicJsonType& object_insert_impl(BasicJsonType& object, const Iterator& /*it*/, const string_t& key, detail::priority_tag<0> /*unused*/)
{
return object_add(object, key);
}
template<typename BasicJsonType, typename Iterator>
static BasicJsonType& object_insert(BasicJsonType& object, const Iterator& it, const string_t& key)
{
return object_insert_impl(object, it, key, detail::priority_tag<1> {});
}
/*!
@brief undoes the change @ref get_unchecked_create made to an existing value
@ref get_unchecked walks existing values without changing them. At the
first reference token that does not select an existing value, @ref
get_unchecked_create makes exactly one change to an existing value: it
converts a null value, inserts a key into an object, or grows an array.
Everything it creates afterwards lies below that change. If a later step
throws, the destructor of this guard reverts that one change, which
removes everything created after it, so the value is left as it was
(strong exception safety). The guard needs no try/catch and therefore also
compiles with JSON_NOEXCEPTION.
Reverting the change must not throw, as it happens in a destructor. It
erases the inserted member by its iterator (no key is converted or
compared, nothing is allocated), erases the added elements at the end of
the array, or swaps the null value back. This holds for std::map,
std::vector, ordered_map, and every object_t, array_t, and
json_base_class_t whose erase() and move operations do not throw.
Between recording and reverting, nothing is inserted into or erased from
the object or array, so the iterator stays valid.
*/
template<typename BasicJsonType>
class get_unchecked_rollback
{
public:
using object_iterator = typename BasicJsonType::object_t::iterator;
/// record that @a member was inserted into the object @a value
get_unchecked_rollback(BasicJsonType* value, const object_iterator& member)
: m_value(value)
, m_member(member)
, m_change(change::key_inserted)
{}
/// record the change that the next reference token makes to the null value or array @a value
explicit get_unchecked_rollback(BasicJsonType* value)
: m_value(value)
{
if (value->is_null())
{
// keep the null value (with its base class and positions) and
// continue with a fresh null value in its place, which is what
// assigning an array or object to it would have left there
m_saved_null.swap(*value);
m_change = change::null_converted;
}
else
{
JSON_ASSERT(value->is_array());
m_old_size = value->m_data.m_value.array->size();
m_change = change::array_grown;
}
}
get_unchecked_rollback(const get_unchecked_rollback&) = delete;
get_unchecked_rollback(get_unchecked_rollback&&) = delete;
get_unchecked_rollback& operator=(const get_unchecked_rollback&) = delete;
get_unchecked_rollback& operator=(get_unchecked_rollback&&) = delete;
~get_unchecked_rollback()
{
if (JSON_HEDLEY_UNLIKELY(m_change != change::none))
{
undo();
}
}
/// keep the change
void commit() noexcept
{
m_change = change::none;
}
private:
JSON_HEDLEY_NEVER_INLINE void undo() noexcept
{
switch (m_change)
{
case change::null_converted:
// the values created since go away with m_saved_null
m_value->swap(m_saved_null);
break;
case change::key_inserted:
m_value->m_data.m_value.object->erase(m_member);
break;
case change::array_grown:
{
auto& elements = *m_value->m_data.m_value.array;
elements.erase(std::next(elements.begin(), static_cast<typename BasicJsonType::difference_type>(m_old_size)), elements.end());
break;
}
case change::none:
default:
break;
}
}
enum class change
{
none,
null_converted,
key_inserted,
array_grown
};
BasicJsonType* m_value;
object_iterator m_member {};
typename BasicJsonType::size_type m_old_size = 0;
change m_change = change::none;
BasicJsonType m_saved_null {};
};
JSON_PRIVATE_UNLESS_TESTED: JSON_PRIVATE_UNLESS_TESTED:
json_pointer top() const json_pointer top() const
{ {
@@ -972,43 +1217,245 @@ class json_pointer
Complexity: Linear in the length of the JSON pointer. Complexity: Linear in the length of the JSON pointer.
@throw parse_error.106 if an array index begins with '0' Exception safety: Strong guarantee. This function walks existing values
@throw parse_error.109 if an array index was not a number until the first reference token that does not select one. If that is the
@throw out_of_range.404 if the JSON pointer can not be resolved last reference token, inserting the key into the object or growing the
array is strongly exception safe by itself; any other change is made by
@ref get_unchecked_create, which undoes it if a later step throws.
@throw parse_error.106 if an array index of an existing array begins with '0'
@throw parse_error.109 if an array index of an existing array was not a number
@throw out_of_range.404 if the JSON pointer can not be resolved, or if an
array index is too large for strtoull
@throw out_of_range.410 if an array index exceeds size_type
@throw std::length_error if padding an array exceeds its maximal size
@throw std::bad_alloc if an allocation fails
*/ */
template<typename BasicJsonType> template<typename BasicJsonType>
BasicJsonType& get_unchecked(BasicJsonType* ptr) const BasicJsonType& get_unchecked(BasicJsonType* ptr) const
{ {
for (const auto& reference_token : reference_tokens) const auto* token = reference_tokens.data();
const auto* const end = token + reference_tokens.size();
if (token == end)
{ {
return *ptr;
}
if (ptr->is_null())
{
// the null value is converted to an array or an object
return get_unchecked_create(ptr, token, end);
}
// walk existing values; the walk itself inserts nothing, which keeps
// the loop fast (insertions are done below or in get_unchecked_create)
typename BasicJsonType::object_t::iterator it;
do
{
if (ptr->is_object())
{
auto& members = *ptr->m_data.m_value.object;
it = object_search(members, *token);
if (!object_search_found(members, it, *token))
{
// the key is inserted below or in get_unchecked_create
break;
}
ptr = &it->second;
}
else if (ptr->is_array())
{
if (is_dash(*token))
{
if (token + 1 == end)
{
// explicitly treat "-" as index beyond the end
return array_append(*ptr);
}
// an element is appended in get_unchecked_create
break;
}
if (token + 1 == end)
{
// convert array index to number; unchecked access
return ptr->operator[](array_index<BasicJsonType>(*token));
}
const auto idx = array_index<BasicJsonType>(*token);
if (idx >= ptr->m_data.m_value.array->size())
{
// the array is padded in get_unchecked_create
break;
}
ptr = &(*ptr->m_data.m_value.array)[idx];
}
else if (ptr->is_null())
{
// the null value is converted in get_unchecked_create
break;
}
else
{
// primitive value with a reference token left
throw_unresolved(*token, ptr);
}
}
while (++token != end);
if (token == end)
{
return *ptr;
}
if (token + 1 == end && ptr->is_object())
{
// insert the missing key where the walk found it belongs
return object_insert(*ptr, it, *token);
}
return get_unchecked_create(ptr, token, end);
}
/*!
@brief create the values for the reference tokens from @a first on
Continues @ref get_unchecked at the first reference token that does not
select an existing value. That token changes @a ptr, an existing value (see
@ref get_unchecked_rollback); all later tokens only change values created
here.
If the emplace() of the object type finds a member that the lookup in @ref
get_unchecked did not find (the two disagree, e.g. for fifo_map), nothing
is inserted, and the walk continues with that member as in @ref
get_unchecked, which is what basic_json::operator[] does.
@param[in] ptr the value the reference token @a first is applied to
@param[in] first the first reference token to resolve (an element of
@ref reference_tokens)
@param[in] end the end of @ref reference_tokens
@throw parse_error.106 if an array index of an existing array begins with
'0' (only after emplace() found a member, see above)
@throw parse_error.109 if an array index of an existing array was not a
number (only after emplace() found a member)
@throw out_of_range.404 if an array index is too large for strtoull, or (only
after emplace() found a member) if the JSON pointer
can not be resolved
@throw out_of_range.410 if an array index exceeds size_type
@throw std::length_error if padding an array exceeds its maximal size
@throw std::bad_alloc if an allocation fails
*/
template<typename BasicJsonType>
JSON_HEDLEY_NEVER_INLINE static BasicJsonType& get_unchecked_create(BasicJsonType* ptr, const string_t* first, const string_t* end)
{
const auto* token = first;
while (ptr->is_object())
{
const auto old_capacity = ptr->container_capacity();
const auto inserted = ptr->m_data.m_value.object->emplace(*token, nullptr);
if (JSON_HEDLEY_LIKELY(inserted.second))
{
get_unchecked_rollback<BasicJsonType> rollback(ptr, inserted.first);
BasicJsonType& result = create_values(&ptr->set_parent_after_insert(inserted.first->second, old_capacity), token + 1, end);
rollback.commit();
return result;
}
// nothing was inserted: walk existing values from the member
// emplace() found on, like get_unchecked
ptr = &inserted.first->second;
while (++token != end)
{
if (ptr->is_object())
{
auto& members = *ptr->m_data.m_value.object;
const auto it = object_search(members, *token);
if (!object_search_found(members, it, *token))
{
break;
}
ptr = &it->second;
}
else if (ptr->is_array())
{
if (is_dash(*token))
{
break;
}
const auto idx = array_index<BasicJsonType>(*token);
if (idx >= ptr->m_data.m_value.array->size())
{
break;
}
ptr = &(*ptr->m_data.m_value.array)[idx];
}
else if (ptr->is_null())
{
break;
}
else
{
// primitive value with a reference token left
throw_unresolved(*token, ptr);
}
}
if (token == end)
{
return *ptr;
}
}
get_unchecked_rollback<BasicJsonType> rollback(ptr);
BasicJsonType& result = create_values(ptr, token, end);
rollback.commit();
return result;
}
/*!
@brief the loop of @ref get_unchecked_create
@a ptr is a null value or an array. Every value created here is a fresh
one, so every object a key is added to is empty.
@throw out_of_range.404 if an array index is too large for strtoull
@throw out_of_range.410 if an array index exceeds size_type
@throw std::length_error if padding an array exceeds its maximal size
@throw std::bad_alloc if an allocation fails
*/
template<typename BasicJsonType>
static BasicJsonType& create_values(BasicJsonType* ptr, const string_t* first, const string_t* end)
{
for (const auto* token = first; token != end; ++token)
{
const auto& reference_token = *token;
// convert null values to arrays or objects before continuing // convert null values to arrays or objects before continuing
if (ptr->is_null()) if (ptr->is_null())
{ {
// check if the reference token is a number // change value to an array for array indices or "-" or to an
const bool nums = // object otherwise; every null value here is a fresh one (see
std::all_of(reference_token.begin(), reference_token.end(), // get_unchecked_rollback), so converting it in place leaves
[](const unsigned char x) // the same value as assigning an array or object to it
if (is_array_index_token(reference_token) || is_dash(reference_token))
{ {
return std::isdigit(x); ptr->template convert_null_to<typename BasicJsonType::array_t>();
}); }
else
// change value to an array for numbers or "-" or to object otherwise {
*ptr = (nums || reference_token == "-") ptr->template convert_null_to<typename BasicJsonType::object_t>();
? detail::value_t::array }
: detail::value_t::object;
} }
if (ptr->is_object()) if (ptr->is_object())
{ {
// use unchecked object access // the object is new and empty
ptr = &object_member(*ptr, reference_token); ptr = &object_add(*ptr, reference_token);
} }
else if (ptr->is_array()) else
{ {
if (reference_token == "-") // only the first value is not created here, and it is a null
// value or an array (see get_unchecked_create)
JSON_ASSERT(ptr->is_array());
if (is_dash(reference_token))
{ {
// explicitly treat "-" as index beyond the end // explicitly treat "-" as index beyond the end
ptr = &ptr->operator[](ptr->m_data.m_value.array->size()); ptr = &array_append(*ptr);
} }
else else
{ {
@@ -1016,16 +1463,31 @@ class json_pointer
ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token)); ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token));
} }
} }
else
{
// primitive value with a reference token left
throw_unresolved(reference_token, ptr);
}
} }
return *ptr; return *ptr;
} }
/// whether @a s is a valid array index, that is a nonempty sequence of
/// digits without a leading '0' (cf. RFC 6901, Sect. 4); tokens that could
/// never be a valid array index (such as "01" or "") are object keys
static bool is_array_index_token(const string_t& s)
{
const std::size_t size = s.size();
if (size == 0 || (size > 1 && s[0] == '0'))
{
return false;
}
for (std::size_t i = 0; i < size; ++i)
{
if (std::isdigit(static_cast<unsigned char>(s[i])) == 0)
{
return false;
}
}
return true;
}
/*! /*!
@throw parse_error.106 if an array index begins with '0' @throw parse_error.106 if an array index begins with '0'
@throw parse_error.109 if an array index was not a number @throw parse_error.109 if an array index was not a number
+1 -1
View File
@@ -7003,7 +7003,6 @@ public:
switch (parent.m_data.m_type) switch (parent.m_data.m_type)
{ {
case value_t::null:
case value_t::object: case value_t::object:
{ {
// use operator[] to add value // use operator[] to add value
@@ -7035,6 +7034,7 @@ public:
// the parent of an "add" target must be an object or array // the parent of an "add" target must be an object or array
// (see #4292) // (see #4292)
case value_t::null:
case value_t::string: case value_t::string:
case value_t::boolean: case value_t::boolean:
case value_t::number_integer: case value_t::number_integer:
+490 -27
View File
@@ -23585,7 +23585,7 @@ NLOHMANN_JSON_NAMESPACE_END
#include <algorithm> // all_of, copy #include <algorithm> // copy
#include <array> // array #include <array> // array
#include <cctype> // isdigit #include <cctype> // isdigit
#include <cerrno> // errno, ERANGE #include <cerrno> // errno, ERANGE
@@ -23593,9 +23593,11 @@ NLOHMANN_JSON_NAMESPACE_END
#include <cstdint> // uint64_t #include <cstdint> // uint64_t
#include <cstdlib> // strtoull #include <cstdlib> // strtoull
#include <cstring> // memchr, memcmp, memcpy #include <cstring> // memchr, memcmp, memcpy
#include <functional> // less
#ifndef JSON_NO_IO #ifndef JSON_NO_IO
#include <iosfwd> // ostream #include <iosfwd> // ostream
#endif // JSON_NO_IO #endif // JSON_NO_IO
#include <iterator> // next
#include <limits> // max #include <limits> // max
#include <string> // string #include <string> // string
#include <utility> // move #include <utility> // move
@@ -23609,6 +23611,8 @@ NLOHMANN_JSON_NAMESPACE_END
// #include <nlohmann/detail/meta/type_traits.hpp> // #include <nlohmann/detail/meta/type_traits.hpp>
// #include <nlohmann/detail/meta/void_t.hpp>
// #include <nlohmann/detail/string_concat.hpp> // #include <nlohmann/detail/string_concat.hpp>
// #include <nlohmann/detail/string_escape.hpp> // #include <nlohmann/detail/string_escape.hpp>
@@ -23719,6 +23723,26 @@ JSON_HEDLEY_ALWAYS_INLINE array_index_status parse_array_index(const StringType&
return array_index_status::ok; return array_index_status::ok;
} }
/// whether two keys of type @a StringType are equivalent under @a Compare
/// exactly if they are equal: std::less<StringType> and std::less<>
template<typename Compare, typename StringType>
struct is_less_comparator : std::false_type {};
template<typename StringType>
struct is_less_comparator<std::less<StringType>, StringType> : std::true_type {};
template<typename StringType>
struct is_less_comparator<std::less<void>, StringType> : std::true_type {};
/// whether the objects of type @a ObjectType find a key of type @a StringType
/// exactly if it equals a stored key
template<typename ObjectType, typename StringType, typename = void>
struct finds_equal_keys : std::false_type {};
template<typename ObjectType, typename StringType>
struct finds_equal_keys<ObjectType, StringType, void_t<typename ObjectType::key_compare, typename ObjectType::key_type>>
: conjunction<is_less_comparator<typename ObjectType::key_compare, StringType>, std::is_same<typename ObjectType::key_type, StringType>> {};
} // namespace detail } // namespace detail
/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document /// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document
@@ -24174,6 +24198,228 @@ class json_pointer
return object_member_impl(object, key, detail::priority_tag<1> {}); return object_member_impl(object, key, detail::priority_tag<1> {});
} }
/// @brief append a null value to @a array, like operator[] with the size of @a array
template<typename BasicJsonType>
static BasicJsonType& array_append(BasicJsonType& array)
{
auto& elements = *array.m_data.m_value.array;
const auto old_capacity = array.container_capacity();
elements.emplace_back();
return array.set_parent_after_insert(elements.back(), old_capacity);
}
/// @brief add @a key, which is missing, to @a object
template<typename BasicJsonType>
static auto object_add_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<1> /*unused*/)
-> decltype(object.m_data.m_value.object->emplace(key, nullptr), std::declval<BasicJsonType&>())
{
const auto old_capacity = object.container_capacity();
const auto it = object.m_data.m_value.object->emplace(key, nullptr).first;
return object.set_parent_after_insert(it->second, old_capacity);
}
template<typename BasicJsonType>
static BasicJsonType& object_add_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<0> /*unused*/)
{
return object.operator[](key);
}
template<typename BasicJsonType>
static BasicJsonType& object_add(BasicJsonType& object, const string_t& key)
{
return object_add_impl(object, key, detail::priority_tag<1> {});
}
/// @brief search @a key in @a members: the result is where the key is or,
/// for objects that can insert at a position (std::map), where it belongs
template<typename ObjectType>
static auto object_search_impl(ObjectType& members, const string_t& key, detail::priority_tag<1> /*unused*/)
-> decltype(members.key_comp()(key, members.lower_bound(key)->first), members.lower_bound(key))
{
return members.lower_bound(key);
}
template<typename ObjectType>
static auto object_search_impl(ObjectType& members, const string_t& key, detail::priority_tag<0> /*unused*/)
-> decltype(members.find(key))
{
return members.find(key);
}
template<typename ObjectType>
static auto object_search(ObjectType& members, const string_t& key)
-> decltype(object_search_impl(members, key, detail::priority_tag<1> {}))
{
return object_search_impl(members, key, detail::priority_tag<1> {});
}
/// @brief whether @a it, the result of @ref object_search for @a key, points to @a key
///
/// With std::less, the key at the lower bound is equivalent to @a key
/// exactly if it is equal to it, and comparing for equality is cheaper.
template<typename ObjectType, typename Iterator,
detail::enable_if_t<detail::finds_equal_keys<ObjectType, string_t>::value, int> = 0>
static auto object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& key, detail::priority_tag<2> /*unused*/)
-> decltype(members.lower_bound(key), it->first == key)
{
return it != members.end() && it->first == key;
}
template<typename ObjectType, typename Iterator>
static auto object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& key, detail::priority_tag<1> /*unused*/)
-> decltype(members.key_comp()(key, it->first), members.lower_bound(key), bool())
{
return it != members.end() && !members.key_comp()(key, it->first);
}
template<typename ObjectType, typename Iterator>
static bool object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& /*key*/, detail::priority_tag<0> /*unused*/)
{
return it != members.end();
}
template<typename ObjectType, typename Iterator>
static bool object_search_found(ObjectType& members, const Iterator& it, const string_t& key)
{
return object_search_found_impl(members, it, key, detail::priority_tag<2> {});
}
/// @brief insert @a key, which is missing, into @a object at @a it, the
/// result of @ref object_search for @a key
template<typename BasicJsonType, typename Iterator>
static auto object_insert_impl(BasicJsonType& object, const Iterator& it, const string_t& key, detail::priority_tag<1> /*unused*/)
-> decltype(object.m_data.m_value.object->emplace_hint(it, key, nullptr), std::declval<BasicJsonType&>())
{
const auto old_capacity = object.container_capacity();
const auto pos = object.m_data.m_value.object->emplace_hint(it, key, nullptr);
return object.set_parent_after_insert(pos->second, old_capacity);
}
template<typename BasicJsonType, typename Iterator>
static BasicJsonType& object_insert_impl(BasicJsonType& object, const Iterator& /*it*/, const string_t& key, detail::priority_tag<0> /*unused*/)
{
return object_add(object, key);
}
template<typename BasicJsonType, typename Iterator>
static BasicJsonType& object_insert(BasicJsonType& object, const Iterator& it, const string_t& key)
{
return object_insert_impl(object, it, key, detail::priority_tag<1> {});
}
/*!
@brief undoes the change @ref get_unchecked_create made to an existing value
@ref get_unchecked walks existing values without changing them. At the
first reference token that does not select an existing value, @ref
get_unchecked_create makes exactly one change to an existing value: it
converts a null value, inserts a key into an object, or grows an array.
Everything it creates afterwards lies below that change. If a later step
throws, the destructor of this guard reverts that one change, which
removes everything created after it, so the value is left as it was
(strong exception safety). The guard needs no try/catch and therefore also
compiles with JSON_NOEXCEPTION.
Reverting the change must not throw, as it happens in a destructor. It
erases the inserted member by its iterator (no key is converted or
compared, nothing is allocated), erases the added elements at the end of
the array, or swaps the null value back. This holds for std::map,
std::vector, ordered_map, and every object_t, array_t, and
json_base_class_t whose erase() and move operations do not throw.
Between recording and reverting, nothing is inserted into or erased from
the object or array, so the iterator stays valid.
*/
template<typename BasicJsonType>
class get_unchecked_rollback
{
public:
using object_iterator = typename BasicJsonType::object_t::iterator;
/// record that @a member was inserted into the object @a value
get_unchecked_rollback(BasicJsonType* value, const object_iterator& member)
: m_value(value)
, m_member(member)
, m_change(change::key_inserted)
{}
/// record the change that the next reference token makes to the null value or array @a value
explicit get_unchecked_rollback(BasicJsonType* value)
: m_value(value)
{
if (value->is_null())
{
// keep the null value (with its base class and positions) and
// continue with a fresh null value in its place, which is what
// assigning an array or object to it would have left there
m_saved_null.swap(*value);
m_change = change::null_converted;
}
else
{
JSON_ASSERT(value->is_array());
m_old_size = value->m_data.m_value.array->size();
m_change = change::array_grown;
}
}
get_unchecked_rollback(const get_unchecked_rollback&) = delete;
get_unchecked_rollback(get_unchecked_rollback&&) = delete;
get_unchecked_rollback& operator=(const get_unchecked_rollback&) = delete;
get_unchecked_rollback& operator=(get_unchecked_rollback&&) = delete;
~get_unchecked_rollback()
{
if (JSON_HEDLEY_UNLIKELY(m_change != change::none))
{
undo();
}
}
/// keep the change
void commit() noexcept
{
m_change = change::none;
}
private:
JSON_HEDLEY_NEVER_INLINE void undo() noexcept
{
switch (m_change)
{
case change::null_converted:
// the values created since go away with m_saved_null
m_value->swap(m_saved_null);
break;
case change::key_inserted:
m_value->m_data.m_value.object->erase(m_member);
break;
case change::array_grown:
{
auto& elements = *m_value->m_data.m_value.array;
elements.erase(std::next(elements.begin(), static_cast<typename BasicJsonType::difference_type>(m_old_size)), elements.end());
break;
}
case change::none:
default:
break;
}
}
enum class change
{
none,
null_converted,
key_inserted,
array_grown
};
BasicJsonType* m_value;
object_iterator m_member {};
typename BasicJsonType::size_type m_old_size = 0;
change m_change = change::none;
BasicJsonType m_saved_null {};
};
JSON_PRIVATE_UNLESS_TESTED: JSON_PRIVATE_UNLESS_TESTED:
json_pointer top() const json_pointer top() const
{ {
@@ -24557,43 +24803,245 @@ class json_pointer
Complexity: Linear in the length of the JSON pointer. Complexity: Linear in the length of the JSON pointer.
@throw parse_error.106 if an array index begins with '0' Exception safety: Strong guarantee. This function walks existing values
@throw parse_error.109 if an array index was not a number until the first reference token that does not select one. If that is the
@throw out_of_range.404 if the JSON pointer can not be resolved last reference token, inserting the key into the object or growing the
array is strongly exception safe by itself; any other change is made by
@ref get_unchecked_create, which undoes it if a later step throws.
@throw parse_error.106 if an array index of an existing array begins with '0'
@throw parse_error.109 if an array index of an existing array was not a number
@throw out_of_range.404 if the JSON pointer can not be resolved, or if an
array index is too large for strtoull
@throw out_of_range.410 if an array index exceeds size_type
@throw std::length_error if padding an array exceeds its maximal size
@throw std::bad_alloc if an allocation fails
*/ */
template<typename BasicJsonType> template<typename BasicJsonType>
BasicJsonType& get_unchecked(BasicJsonType* ptr) const BasicJsonType& get_unchecked(BasicJsonType* ptr) const
{ {
for (const auto& reference_token : reference_tokens) const auto* token = reference_tokens.data();
const auto* const end = token + reference_tokens.size();
if (token == end)
{ {
return *ptr;
}
if (ptr->is_null())
{
// the null value is converted to an array or an object
return get_unchecked_create(ptr, token, end);
}
// walk existing values; the walk itself inserts nothing, which keeps
// the loop fast (insertions are done below or in get_unchecked_create)
typename BasicJsonType::object_t::iterator it;
do
{
if (ptr->is_object())
{
auto& members = *ptr->m_data.m_value.object;
it = object_search(members, *token);
if (!object_search_found(members, it, *token))
{
// the key is inserted below or in get_unchecked_create
break;
}
ptr = &it->second;
}
else if (ptr->is_array())
{
if (is_dash(*token))
{
if (token + 1 == end)
{
// explicitly treat "-" as index beyond the end
return array_append(*ptr);
}
// an element is appended in get_unchecked_create
break;
}
if (token + 1 == end)
{
// convert array index to number; unchecked access
return ptr->operator[](array_index<BasicJsonType>(*token));
}
const auto idx = array_index<BasicJsonType>(*token);
if (idx >= ptr->m_data.m_value.array->size())
{
// the array is padded in get_unchecked_create
break;
}
ptr = &(*ptr->m_data.m_value.array)[idx];
}
else if (ptr->is_null())
{
// the null value is converted in get_unchecked_create
break;
}
else
{
// primitive value with a reference token left
throw_unresolved(*token, ptr);
}
}
while (++token != end);
if (token == end)
{
return *ptr;
}
if (token + 1 == end && ptr->is_object())
{
// insert the missing key where the walk found it belongs
return object_insert(*ptr, it, *token);
}
return get_unchecked_create(ptr, token, end);
}
/*!
@brief create the values for the reference tokens from @a first on
Continues @ref get_unchecked at the first reference token that does not
select an existing value. That token changes @a ptr, an existing value (see
@ref get_unchecked_rollback); all later tokens only change values created
here.
If the emplace() of the object type finds a member that the lookup in @ref
get_unchecked did not find (the two disagree, e.g. for fifo_map), nothing
is inserted, and the walk continues with that member as in @ref
get_unchecked, which is what basic_json::operator[] does.
@param[in] ptr the value the reference token @a first is applied to
@param[in] first the first reference token to resolve (an element of
@ref reference_tokens)
@param[in] end the end of @ref reference_tokens
@throw parse_error.106 if an array index of an existing array begins with
'0' (only after emplace() found a member, see above)
@throw parse_error.109 if an array index of an existing array was not a
number (only after emplace() found a member)
@throw out_of_range.404 if an array index is too large for strtoull, or (only
after emplace() found a member) if the JSON pointer
can not be resolved
@throw out_of_range.410 if an array index exceeds size_type
@throw std::length_error if padding an array exceeds its maximal size
@throw std::bad_alloc if an allocation fails
*/
template<typename BasicJsonType>
JSON_HEDLEY_NEVER_INLINE static BasicJsonType& get_unchecked_create(BasicJsonType* ptr, const string_t* first, const string_t* end)
{
const auto* token = first;
while (ptr->is_object())
{
const auto old_capacity = ptr->container_capacity();
const auto inserted = ptr->m_data.m_value.object->emplace(*token, nullptr);
if (JSON_HEDLEY_LIKELY(inserted.second))
{
get_unchecked_rollback<BasicJsonType> rollback(ptr, inserted.first);
BasicJsonType& result = create_values(&ptr->set_parent_after_insert(inserted.first->second, old_capacity), token + 1, end);
rollback.commit();
return result;
}
// nothing was inserted: walk existing values from the member
// emplace() found on, like get_unchecked
ptr = &inserted.first->second;
while (++token != end)
{
if (ptr->is_object())
{
auto& members = *ptr->m_data.m_value.object;
const auto it = object_search(members, *token);
if (!object_search_found(members, it, *token))
{
break;
}
ptr = &it->second;
}
else if (ptr->is_array())
{
if (is_dash(*token))
{
break;
}
const auto idx = array_index<BasicJsonType>(*token);
if (idx >= ptr->m_data.m_value.array->size())
{
break;
}
ptr = &(*ptr->m_data.m_value.array)[idx];
}
else if (ptr->is_null())
{
break;
}
else
{
// primitive value with a reference token left
throw_unresolved(*token, ptr);
}
}
if (token == end)
{
return *ptr;
}
}
get_unchecked_rollback<BasicJsonType> rollback(ptr);
BasicJsonType& result = create_values(ptr, token, end);
rollback.commit();
return result;
}
/*!
@brief the loop of @ref get_unchecked_create
@a ptr is a null value or an array. Every value created here is a fresh
one, so every object a key is added to is empty.
@throw out_of_range.404 if an array index is too large for strtoull
@throw out_of_range.410 if an array index exceeds size_type
@throw std::length_error if padding an array exceeds its maximal size
@throw std::bad_alloc if an allocation fails
*/
template<typename BasicJsonType>
static BasicJsonType& create_values(BasicJsonType* ptr, const string_t* first, const string_t* end)
{
for (const auto* token = first; token != end; ++token)
{
const auto& reference_token = *token;
// convert null values to arrays or objects before continuing // convert null values to arrays or objects before continuing
if (ptr->is_null()) if (ptr->is_null())
{ {
// check if the reference token is a number // change value to an array for array indices or "-" or to an
const bool nums = // object otherwise; every null value here is a fresh one (see
std::all_of(reference_token.begin(), reference_token.end(), // get_unchecked_rollback), so converting it in place leaves
[](const unsigned char x) // the same value as assigning an array or object to it
if (is_array_index_token(reference_token) || is_dash(reference_token))
{ {
return std::isdigit(x); ptr->template convert_null_to<typename BasicJsonType::array_t>();
}); }
else
// change value to an array for numbers or "-" or to object otherwise {
*ptr = (nums || reference_token == "-") ptr->template convert_null_to<typename BasicJsonType::object_t>();
? detail::value_t::array }
: detail::value_t::object;
} }
if (ptr->is_object()) if (ptr->is_object())
{ {
// use unchecked object access // the object is new and empty
ptr = &object_member(*ptr, reference_token); ptr = &object_add(*ptr, reference_token);
} }
else if (ptr->is_array()) else
{ {
if (reference_token == "-") // only the first value is not created here, and it is a null
// value or an array (see get_unchecked_create)
JSON_ASSERT(ptr->is_array());
if (is_dash(reference_token))
{ {
// explicitly treat "-" as index beyond the end // explicitly treat "-" as index beyond the end
ptr = &ptr->operator[](ptr->m_data.m_value.array->size()); ptr = &array_append(*ptr);
} }
else else
{ {
@@ -24601,16 +25049,31 @@ class json_pointer
ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token)); ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token));
} }
} }
else
{
// primitive value with a reference token left
throw_unresolved(reference_token, ptr);
}
} }
return *ptr; return *ptr;
} }
/// whether @a s is a valid array index, that is a nonempty sequence of
/// digits without a leading '0' (cf. RFC 6901, Sect. 4); tokens that could
/// never be a valid array index (such as "01" or "") are object keys
static bool is_array_index_token(const string_t& s)
{
const std::size_t size = s.size();
if (size == 0 || (size > 1 && s[0] == '0'))
{
return false;
}
for (std::size_t i = 0; i < size; ++i)
{
if (std::isdigit(static_cast<unsigned char>(s[i])) == 0)
{
return false;
}
}
return true;
}
/*! /*!
@throw parse_error.106 if an array index begins with '0' @throw parse_error.106 if an array index begins with '0'
@throw parse_error.109 if an array index was not a number @throw parse_error.109 if an array index was not a number
@@ -42793,7 +43256,6 @@ public:
switch (parent.m_data.m_type) switch (parent.m_data.m_type)
{ {
case value_t::null:
case value_t::object: case value_t::object:
{ {
// use operator[] to add value // use operator[] to add value
@@ -42825,6 +43287,7 @@ public:
// the parent of an "add" target must be an object or array // the parent of an "add" target must be an object or array
// (see #4292) // (see #4292)
case value_t::null:
case value_t::string: case value_t::string:
case value_t::boolean: case value_t::boolean:
case value_t::number_integer: case value_t::number_integer:
+17
View File
@@ -373,6 +373,23 @@ TEST_CASE("alternative string type")
CHECK_FALSE(j.contains(alt_json::json_pointer("/foo/bar"))); CHECK_FALSE(j.contains(alt_json::json_pointer("/foo/bar")));
} }
SECTION("non-const operator[](json_pointer)")
{
// creating values must compile without string_t::begin()
alt_json j = alt_json::parse(R"({"a":{"b":[1]},"c":null})");
const alt_json before = j;
j[alt_json::json_pointer("/a/b/3/x/-/y")] = 1; // pad, append, create
j[alt_json::json_pointer("/a/z")] = 2; // insert a key
j[alt_json::json_pointer("/c/0/q")] = 3; // convert null
j[alt_json::json_pointer("/a/b/-")] = 4; // append
CHECK(j.dump() == R"({"a":{"b":[1,null,null,{"x":[{"y":1}]},4],"z":2},"c":[{"q":3}]})");
// a failing access leaves the value unchanged
alt_json k = before;
CHECK_THROWS_AS(k[alt_json::json_pointer("/a/n/99999999999999999999999")], alt_json::out_of_range&);
CHECK(k == before);
}
SECTION("operator/(std::size_t)") SECTION("operator/(std::size_t)")
{ {
// json_pointer::operator/=(std::size_t) must compile without string_t // json_pointer::operator/=(std::size_t) must compile without string_t
+223 -1
View File
@@ -15,6 +15,7 @@ using nlohmann::json;
#endif #endif
#include <fstream> #include <fstream>
#include <limits>
#include <string> #include <string>
#include <vector> #include <vector>
#include "make_test_data_available.hpp" #include "make_test_data_available.hpp"
@@ -490,7 +491,9 @@ TEST_CASE("JSON patch")
} }
// A.13. Invalid JSON Patch Document // A.13. Invalid JSON Patch Document
// not applicable // not applicable: the library cannot tell the duplicate "op" members
// apart, because the parser resolves them (the last one is kept); see
// "duplicate member names in an operation" below
SECTION("A.14. Escape Ordering") SECTION("A.14. Escape Ordering")
{ {
@@ -1515,6 +1518,24 @@ TEST_CASE("JSON patch - add to a primitive parent (regression #4292)")
#endif #endif
} }
SECTION("null parent")
{
json const doc = {{"foo", nullptr}};
json const patch = {{{"op", "add"}, {"path", "/foo/bar"}, {"value", 1}}};
#if JSON_DIAGNOSTICS
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.411] (/foo) cannot add value: the JSON Patch 'add' target's parent is of type null, but must be an object or array", json::out_of_range&);
#else
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.411] cannot add value: the JSON Patch 'add' target's parent is of type null, but must be an object or array", json::out_of_range&);
#endif
}
SECTION("null document")
{
json const doc = nullptr;
json const patch = {{{"op", "add"}, {"path", "/-"}, {"value", 1}}};
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.411] cannot add value: the JSON Patch 'add' target's parent is of type null, but must be an object or array", json::out_of_range&);
}
SECTION("number parent") SECTION("number parent")
{ {
json const doc = {{"foo", 1}}; json const doc = {{"foo", 1}};
@@ -2103,3 +2124,204 @@ TEST_CASE_TEMPLATE("JSON patch - diff round trip on hand-made documents", Json,
roundtrip(Json(1), Json::parse("[1]")); roundtrip(Json(1), Json::parse("[1]"));
} }
} }
TEST_CASE("JSON patch - behavior documented in the API reference")
{
SECTION("duplicate member names in an operation")
{
// RFC 6902, A.13: the parser resolves the duplicates (the last one
// is kept), so this is a "remove" and not an "add"
const json patch = json::parse(R"([{"op": "add", "op": "remove", "path": "/a"}])");
CHECK(patch == json::parse(R"([{"op": "remove", "path": "/a"}])"));
CHECK(json({{"a", 1}, {"b", 2}}).patch(patch) == json({{"b", 2}}));
}
SECTION("add with a leading zero in an array index")
{
const json patch = R"([{"op": "add", "path": "/01", "value": 1}])"_json;
json doc = json::array();
#if !defined(JSON_NOEXCEPTION)
CHECK_THROWS_AS(doc.patch(patch), json::parse_error&);
CHECK_THROWS_AS(doc.patch_inplace(patch), json::parse_error&);
#endif
CHECK(doc == json::array());
}
SECTION("test compares numbers by value")
{
const json doc = R"({"a": 1, "b": 0, "c": 1.5})"_json;
CHECK(doc.patch(R"([{"op": "test", "path": "/a", "value": 1.0}])"_json) == doc);
CHECK(doc.patch(R"([{"op": "test", "path": "/b", "value": -0.0}])"_json) == doc);
CHECK(doc.patch(R"([{"op": "test", "path": "/c", "value": 1.5}])"_json) == doc);
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/a", "value": 2}])"_json), json::other_error&);
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/a", "value": "1"}])"_json), json::other_error&);
}
SECTION("test ignores the order of object members, but not the order of array elements")
{
const json doc = R"({"o": {"x": 1, "y": 2}, "a": [1, 2]})"_json;
CHECK(doc.patch(R"([{"op": "test", "path": "/o", "value": {"y": 2, "x": 1}}])"_json) == doc);
CHECK(doc.patch(R"([{"op": "test", "path": "/a", "value": [1, 2]}])"_json) == doc);
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/a", "value": [2, 1]}])"_json), json::other_error&);
}
SECTION("test on a path that does not exist")
{
const json doc = R"({"a": 1})"_json;
// with a "value" member, the failed lookup is a failed test
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/b", "value": 1}])"_json), json::other_error&);
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/b", "value": null}])"_json), json::other_error&);
// without it, an exception is thrown, too; which one depends on the
// order of evaluation of the operands of ==, so it is not checked
CHECK_THROWS(doc.patch(R"([{"op": "test", "path": "/b"}])"_json));
}
SECTION("patch() has the strong guarantee")
{
const json doc = R"({"a": [1, 2], "o": {"k": "v"}})"_json;
const json before = doc;
const std::vector<json> failing =
{
// remove of a member that does not exist
R"([{"op": "add", "path": "/n", "value": 1}, {"op": "remove", "path": "/missing"}])"_json,
// replace of a member that does not exist
R"([{"op": "remove", "path": "/a/0"}, {"op": "replace", "path": "/missing", "value": 1}])"_json,
// move from a member that does not exist
R"([{"op": "add", "path": "/n", "value": 1}, {"op": "move", "from": "/missing", "path": "/m"}])"_json,
// copy from a member that does not exist
R"([{"op": "add", "path": "/n", "value": 1}, {"op": "copy", "from": "/missing", "path": "/m"}])"_json,
// out-of-range array index
R"([{"op": "add", "path": "/n", "value": 1}, {"op": "add", "path": "/a/5", "value": 1}])"_json,
// failing test after a successful operation
R"([{"op": "remove", "path": "/o"}, {"op": "test", "path": "/a", "value": []}])"_json,
};
for (const auto& patch : failing)
{
CAPTURE(patch.dump())
CHECK_THROWS_AS(doc.patch(patch), json::exception&);
CHECK(doc == before);
json copy = doc;
CHECK_THROWS_AS(copy.patch(patch), json::exception&);
CHECK(copy == before);
}
}
}
TEST_CASE("JSON patch - diff behavior documented in the API reference")
{
SECTION("path is a verbatim prefix")
{
// not escaped and not validated
CHECK(json::diff(1, 2) == R"([{"op": "replace", "path": "", "value": 2}])"_json);
CHECK(json::diff(1, 2, "abc") == R"([{"op": "replace", "path": "abc", "value": 2}])"_json);
CHECK(json::diff(1, 2, "/root") == R"([{"op": "replace", "path": "/root", "value": 2}])"_json);
// an escaped token in a non-root prefix is kept as given
CHECK(json::diff(R"({"a": 1})"_json, R"({"a": 2})"_json, "/x~1y/~0")
== R"([{"op": "replace", "path": "/x~1y/~0/a", "value": 2}])"_json);
CHECK(json::diff(R"({"a": 1})"_json, R"({"a": 2})"_json, "/p")
== R"([{"op": "replace", "path": "/p/a", "value": 2}])"_json);
}
SECTION("keys are escaped in the paths")
{
const json source = json::object();
const json target =
{
{"", 0}, {"/", 1}, {"~", 2}, {"~1", 3}, {"~01", 4}
};
const json patch = json::diff(source, target);
REQUIRE(patch.size() == 5);
// the order of the operations is not checked
json paths = json::object();
for (const auto& op : patch)
{
CHECK(op["op"] == "add");
paths[op["path"].get<std::string>()] = op["value"];
}
CHECK(paths == json({{"/", 0}, {"/~1", 1}, {"/~0", 2}, {"/~01", 3}, {"/~001", 4}}));
CHECK(source.patch(patch) == target);
}
SECTION("NaN is not equal to itself")
{
const json nan = std::numeric_limits<json::number_float_t>::quiet_NaN();
const json patch = json::diff(nan, nan);
CHECK(patch.size() == 1);
CHECK(patch[0]["op"] == "replace");
CHECK(patch[0]["path"] == "");
CHECK(patch[0]["value"].is_number_float());
CHECK(nan.patch(patch) != nan);
// a NaN in a patch is serialized as null
CHECK(json::parse(patch.dump())[0]["value"].is_null());
const json nested = {{"a", {1, nan}}};
const json nested_patch = json::diff(nested, nested);
CHECK(nested_patch.size() == 1);
CHECK(nested_patch[0]["path"] == "/a/1");
// infinity is equal to itself
const json inf = std::numeric_limits<json::number_float_t>::infinity();
CHECK(json::diff(inf, inf).empty());
}
SECTION("discarded values are not equal to themselves")
{
const json discarded(json::value_t::discarded);
const json patch = json::diff(discarded, discarded);
CHECK(patch.size() == 1);
CHECK(patch[0]["op"] == "replace");
CHECK(patch[0]["path"] == "");
CHECK(patch[0]["value"].is_discarded());
CHECK(discarded.patch(patch) != discarded);
}
SECTION("numbers that compare equal produce no operation")
{
CHECK(json::diff(json(1), json(1.0)).empty());
CHECK(json::diff(json(1.0), json(1)).empty());
CHECK(json::diff(json(1U), json(1)).empty());
CHECK(json::diff(json(1), json(1U)).empty());
CHECK(json::diff(json(0.0), json(-0.0)).empty());
// the patched value keeps the number type of the source
const json source = 1;
const json patched = source.patch(json::diff(source, json(1.0)));
CHECK(patched.is_number_integer());
CHECK(patched == json(1.0));
// different values are replaced
CHECK(json::diff(json(1), json(2.0)) == R"([{"op": "replace", "path": "", "value": 2.0}])"_json);
CHECK(json::diff(json(1), json(-1)) == R"([{"op": "replace", "path": "", "value": -1}])"_json);
CHECK(json::diff(json(1.0), json(1.5)) == R"([{"op": "replace", "path": "", "value": 1.5}])"_json);
CHECK(json::diff(json(1), json("1")) == R"([{"op": "replace", "path": "", "value": "1"}])"_json);
}
SECTION("binary values")
{
const json a = json::binary({1, 2});
const json b = json::binary({1, 3});
CHECK(json::diff(a, a).empty());
const json patch = json::diff(a, b);
CHECK(patch.size() == 1);
CHECK(patch[0]["op"] == "replace");
CHECK(patch[0]["path"] == "");
CHECK(patch[0]["value"].is_binary());
CHECK(a.patch(patch) == b);
// serialized, the binary value is an object
const json parsed = json::parse(patch.dump());
CHECK(parsed[0]["value"].is_object());
CHECK(parsed[0]["value"] == R"({"bytes": [1, 3], "subtype": null})"_json);
}
SECTION("a difference as a whole is a replace of the root")
{
CHECK(json::diff(json::array({1}), json::object()) == R"([{"op": "replace", "path": "", "value": {}}])"_json);
CHECK(json::diff(json(nullptr), json::array()) == R"([{"op": "replace", "path": "", "value": []}])"_json);
}
}
+188
View File
@@ -29,9 +29,64 @@ using nlohmann::json;
#include <map> #include <map>
#include <random> #include <random>
#include <sstream> #include <sstream>
#include <stdexcept>
#include <string> #include <string>
#include <vector> #include <vector>
namespace
{
// the non-const operator[] must throw for pointer and leave the value parsed
// from init unchanged (strong exception safety)
template<typename Json>
void check_strong_guarantee(const char* init, const std::string& pointer, const char* message)
{
CAPTURE(init)
CAPTURE(pointer)
Json j = Json::parse(init);
const Json before = Json::parse(init);
const typename Json::json_pointer ptr(pointer);
CHECK_THROWS_WITH_AS(j[ptr], message, typename Json::exception&);
CHECK(j == before);
}
// as above, for an index that fits into size_type, but not into an array
template<typename Json>
void check_strong_guarantee_length_error(const char* init, const std::string& pointer)
{
CAPTURE(init)
CAPTURE(pointer)
Json j = Json::parse(init);
const Json before = Json::parse(init);
const typename Json::json_pointer ptr(pointer);
CHECK_THROWS_AS(j[ptr], std::length_error&);
CHECK(j == before);
}
template<typename Json>
void check_strong_guarantee_cases()
{
const char* const unresolved = "[json.exception.out_of_range.404] unresolved reference token '99999999999999999999999'";
// the first change converts a null value
check_strong_guarantee<Json>("null", "/99999999999999999999999", unresolved);
check_strong_guarantee<Json>("null", "/a/99999999999999999999999", unresolved);
check_strong_guarantee<Json>(R"({"a":null})", "/a/b/-/99999999999999999999999", unresolved);
// the first change inserts a key into an object
check_strong_guarantee<Json>("{}", "/a/99999999999999999999999", unresolved);
check_strong_guarantee<Json>(R"({"a":{"b":1}})", "/a/c/d/99999999999999999999999", unresolved);
// the first change pads or appends to an array
check_strong_guarantee<Json>(R"({"a":[1]})", "/a/5/99999999999999999999999", unresolved);
check_strong_guarantee<Json>(R"({"a":[1]})", "/a/-/x/1/99999999999999999999999", unresolved);
// an index that does not fit into size_type
check_strong_guarantee<Json>("null", "/x/18446744073709551615",
"[json.exception.out_of_range.410] array index 18446744073709551615 exceeds size_type");
// an index that fits into size_type, but not into an array
const auto too_large = std::to_string((std::numeric_limits<typename Json::size_type>::max)() - 1);
check_strong_guarantee_length_error<Json>("null", "/" + too_large);
check_strong_guarantee_length_error<Json>(R"({"a":[[1]]})", "/a/0/3/" + too_large);
}
} // namespace
TEST_CASE("JSON pointers") TEST_CASE("JSON pointers")
{ {
SECTION("errors") SECTION("errors")
@@ -493,6 +548,89 @@ TEST_CASE("JSON pointers")
} }
} }
SECTION("creating intermediate levels")
{
SECTION("tokens that are valid array indices create arrays")
{
json j;
j["/0"_json_pointer] = 1;
CHECK(j == json({1}));
json j2;
j2["/2"_json_pointer] = 1;
CHECK(j2 == json({nullptr, nullptr, 1}));
json j3;
j3["/-"_json_pointer] = 1;
CHECK(j3 == json({1}));
json j4;
j4["/foo/0/0"_json_pointer] = 1;
CHECK(j4 == json({{"foo", {{1}}}}));
}
SECTION("tokens that are no valid array indices create objects")
{
json j;
j["/one"_json_pointer] = 1;
CHECK(j == json({{"one", 1}}));
// leading '0' can never be a valid array index (RFC 6901, Sect. 4)
json j2;
j2["/01"_json_pointer] = 1;
CHECK(j2 == json({{"01", 1}}));
// the empty token is a valid object key, but no valid array index
json j3;
j3["/"_json_pointer] = 1;
CHECK(j3 == json({{"", 1}}));
}
SECTION("creating a level yields the same result as reusing it (#5357)")
{
json j;
j["/a/b/01/d"_json_pointer] = "value";
json j_init = json::object();
j_init["/a/b"_json_pointer] = json::object();
j_init["/a/b/01/d"_json_pointer] = "value";
const json expected = json::parse(R"({"a":{"b":{"01":{"d":"value"}}}})");
CHECK(j == expected);
CHECK(j_init == expected);
// unflatten uses the same key
const json flat = {{"/a/b/01/d", "value"}};
CHECK(flat.unflatten() == expected);
}
SECTION("existing arrays still reject invalid indices")
{
json j = {1, 2, 3};
CHECK_THROWS_WITH_AS(j["/01"_json_pointer],
"[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
CHECK_THROWS_WITH_AS(j.at("/01"_json_pointer),
"[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
}
SECTION("a failing access leaves the value unchanged")
{
check_strong_guarantee_cases<json>();
check_strong_guarantee_cases<nlohmann::ordered_json>();
}
SECTION("a successful access still creates the same values")
{
json j = {{"a", {1}}};
j["/a/3/x/-/y"_json_pointer] = 1;
CHECK(j == json::parse(R"({"a":[1,null,null,{"x":[{"y":1}]}]})"));
j["/a/-"_json_pointer] = 2;
j["/a/0"_json_pointer] = 3;
j["/b"_json_pointer] = 4;
CHECK(j == json::parse(R"({"a":[3,null,null,{"x":[{"y":1}]},2],"b":4})"));
}
}
SECTION("flatten") SECTION("flatten")
{ {
json j = json j =
@@ -1044,6 +1182,56 @@ TEST_CASE("flatten of structured values")
CHECK(flat_object.unflatten() == nlohmann::ordered_json::parse(R"({"b": {"1": 2}})")); CHECK(flat_object.unflatten() == nlohmann::ordered_json::parse(R"({"b": {"1": 2}})"));
} }
#if !defined(JSON_NOEXCEPTION)
SECTION("an object with the key \"0\" next to a non-index key does not round-trip")
{
// the third documented case in which j != j.flatten().unflatten()
const json value = {{"a", {{"0", 1}, {"b", 2}}}};
const json flat = value.flatten();
CHECK(flat == json({{"/a/0", 1}, {"/a/b", 2}}));
CHECK_THROWS_WITH_AS(flat.unflatten(),
"[json.exception.parse_error.109] parse error: array index 'b' is not a number", json::parse_error&);
// the same for other keys that are not valid array indices
CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/1a", 2}}).unflatten(),
"[json.exception.out_of_range.404] unresolved reference token '1a'", json::out_of_range&);
CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/-", 2}}).unflatten(),
"[json.exception.out_of_range.404] unresolved reference token '-'", json::out_of_range&);
}
SECTION("unflatten with the empty token among array indices")
{
CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/", 2}}).unflatten(),
"[json.exception.out_of_range.404] unresolved reference token ''", json::out_of_range&);
CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/1", 2}, {"/a/", 3}}).unflatten(),
"[json.exception.out_of_range.404] unresolved reference token ''", json::out_of_range&);
// without an index "0", the empty token is an ordinary object key
CHECK(json({{"/a/", 2}}).unflatten() == json({{"a", {{"", 2}}}}));
}
SECTION("value() with the empty pointer on a primitive root")
{
// unlike at(), value() rejects primitive values before it looks at the pointer
const json number = 1;
CHECK(number.at(""_json_pointer) == 1);
CHECK_THROWS_WITH_AS(number.value(""_json_pointer, 7),
"[json.exception.type_error.306] cannot use value() with number", json::type_error&);
const json string = "s";
CHECK_THROWS_WITH_AS(string.value(""_json_pointer, 7),
"[json.exception.type_error.306] cannot use value() with string", json::type_error&);
// contains() accepts the empty pointer on every value
CHECK(number.contains(""_json_pointer));
CHECK_FALSE(number.contains("/a"_json_pointer));
// arrays and objects return the default for a missing location
CHECK(json::array().value(""_json_pointer, json(7)) == json::array());
CHECK(json::array().value("/0"_json_pointer, 7) == 7);
}
#endif
SECTION("objects and arrays interleaved") SECTION("objects and arrays interleaved")
{ {
const json value = const json value =
+83
View File
@@ -423,3 +423,86 @@ TEST_CASE("merge_patch() with an argument that aliases *this (#5641)")
} }
} }
} }
TEST_CASE("JSON Merge Patch on non-object, discarded, and binary values")
{
SECTION("an empty object patch turns a non-object target into an empty object")
{
json target = 1;
target.merge_patch(json::object());
CHECK(target == json::object());
json from_null;
from_null.merge_patch(json::object());
CHECK(from_null == json::object());
}
SECTION("a non-object patch replaces a deeply nested target")
{
// checked by type, since == recurses
SECTION("number")
{
json target = json::parse(nested_objects(1000, 0));
target.merge_patch(json(42));
CHECK(target.is_number_integer());
CHECK(target.get<int>() == 42);
}
SECTION("array")
{
json target = json::parse(nested_objects(1000, 0));
target.merge_patch(json({1, nullptr}));
CHECK(target.is_array());
CHECK(target.size() == 2);
CHECK(target[1].is_null());
}
SECTION("null")
{
json target = json::parse(nested_objects(1000, 0));
target.merge_patch(json(nullptr));
CHECK(target.is_null());
}
}
SECTION("discarded values")
{
const json discarded(json::value_t::discarded);
// a discarded patch is not an object and replaces the target
json target = {{"a", 1}};
target.merge_patch(discarded);
CHECK(target.is_discarded());
// a discarded target is replaced by an empty object first
json discarded_target(json::value_t::discarded);
discarded_target.merge_patch(json({{"a", 1}}));
CHECK(discarded_target == json({{"a", 1}}));
json both(json::value_t::discarded);
both.merge_patch(discarded);
CHECK(both.is_discarded());
}
SECTION("binary values")
{
const json bin = json::binary({1, 2, 3}, 7);
// a binary patch is not an object and replaces the target
json target = {{"a", 1}};
target.merge_patch(bin);
CHECK(target.is_binary());
CHECK(target.get_binary() == bin.get_binary());
// a binary target is replaced by an object
json binary_target = bin;
binary_target.merge_patch(json({{"a", 1}}));
CHECK(binary_target == json({{"a", 1}}));
// binary values nested in the patch are kept
json nested = json::object();
nested.merge_patch(json({{"a", bin}}));
CHECK(nested["a"].is_binary());
CHECK(nested["a"].get_binary() == bin.get_binary());
}
}
+17
View File
@@ -1499,6 +1499,23 @@ TEST_CASE("regression tests 1")
CHECK(order.dump() == R"({"a":2,"m":{"y":1,"b":2},"new_key":3})"); CHECK(order.dump() == R"({"a":2,"m":{"y":1,"b":2},"new_key":3})");
} }
SECTION("operator[](json_pointer) when emplace() finds a key the lookup does not")
{
// a copied fifo_map restarts its timestamps, so a new key "k" gets
// the timestamp of "x": the lookup does not find "k", but emplace()
// finds "x" and inserts nothing; the pointer then continues with the
// member of "x", as basic_json::operator[] does
const my_json orig = my_json::parse(R"({"x":1})");
const std::array<const char*, 3> pointers = {{"/k/0", "/k/a/b", "/k/-"}};
for (const char* pointer : pointers)
{
CAPTURE(pointer)
my_json j = orig;
CHECK_THROWS_AS(j[my_json::json_pointer(pointer)], my_json::out_of_range&);
CHECK(j.dump() == R"({"x":1})");
}
}
SECTION("issue #977 - Assigning between different json types") SECTION("issue #977 - Assigning between different json types")
{ {
foo_json lj = ns::foo{3}; foo_json lj = ns::foo{3};