Compare commits

..
Author SHA1 Message Date
Niels Lohmann dc95cf5fa8 docs: correct JSON Pointer documentation and add tests
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 12:07:45 +01:00
16 changed files with 106 additions and 316 deletions

No files matched your search

+9 -2
View File
@@ -61,11 +61,18 @@ Strong exception safety: if an exception occurs, the original value stays intact
## 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
- 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.
- 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
+4 -41
View File
@@ -2,27 +2,17 @@
```cpp
static basic_json diff(const basic_json& source,
const basic_json& target,
const string_t& path = "");
const basic_json& target);
```
Creates a [JSON Patch](http://jsonpatch.com) so that value `source` can be changed into the value `target` by calling
[`patch`](patch.md) function.
For two JSON values `source` and `target`, the following code yields `#!cpp true`:
For two JSON values `source` and `target`, the following code always yields `#!cpp true`:
```cpp
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
`source` (in)
@@ -31,12 +21,6 @@ The equality holds up to [`operator==`](operator_eq.md), so it has the following
`target` (in)
: 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
a JSON patch to convert the `source` to `target`
@@ -47,32 +31,11 @@ Strong guarantee: `source` and `target` are never modified.
## Complexity
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`.
Linear in the lengths of `source` and `target`.
## Notes
- 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).
Currently, only `remove`, `add`, and `replace` operations are generated.
## Examples
+2 -10
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:
`"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
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
pointer inside the patch cannot be resolved, e.g., `-` in a "remove" operation or `1a` for an array; example:
`"unresolved reference token '-'"`.
@@ -53,10 +53,7 @@ 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"
location is a proper prefix of its "path" location.
- Throws [`other_error.501`](../../home/exceptions.md#jsonexceptionother_error501) if "test" operation was
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.
unsuccessful.
## Complexity
@@ -68,10 +65,6 @@ 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
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
??? example "Example: apply a JSON patch"
@@ -115,7 +108,6 @@ is kept, see [`object_t`](object_t.md#behavior)), so `#!json {"op": "add", "op":
- 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
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
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
@@ -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:
`"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
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
pointer inside the patch cannot be resolved, e.g., `-` in a "remove" operation or `1a` for an array; example:
`"unresolved reference token '-'"`.
@@ -49,10 +49,7 @@ 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"
location is a proper prefix of its "path" location.
- Throws [`other_error.501`](../../home/exceptions.md#jsonexceptionother_error501) if "test" operation was
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.
unsuccessful.
## Complexity
@@ -65,10 +62,6 @@ 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
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
??? example "Example: apply a JSON patch in place"
@@ -113,7 +106,6 @@ is kept, see [`object_t`](object_t.md#behavior)), so `#!json {"op": "add", "op":
- 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
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
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
+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
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
(because one of its keys is `0`) and another key at that level begins with a digit but is not a valid array index
(such as `1a`), or is `-`; example:
`"unresolved reference token '-'"`
(because one of its keys is `0`) and another key at that level is empty (as in `/a/`), is `-`, or begins with a digit
but is not a valid array index (such as `1a`); examples: `"unresolved reference token '-'"`,
`"unresolved reference token ''"`
## Complexity
@@ -46,7 +46,7 @@ Linear in the size of the JSON value.
## 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.
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
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()`.
## Examples
+3
View File
@@ -45,6 +45,9 @@ This is equivalent to Python's `dict.get(key, default)`.
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[]`"
@@ -12,7 +12,8 @@ Parent of this JSON pointer; in case this JSON pointer is the root, the root its
## 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
+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
value is not UTF-8 encoded. Note that unlike the [`dump`](basic_json/dump.md) member functions, no `error_handler`
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
+1 -18
View File
@@ -13,12 +13,6 @@ 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,
[`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"
The following code shows how a JSON patch is applied to a value.
@@ -47,23 +41,12 @@ flowchart LR
!!! success "Invariant"
For two JSON values *source* and *target*, the following code yields true:
For two JSON values *source* and *target*, the following code yields always true:
```cpp
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"
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
[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 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
[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
JSON Pointers can be used in the [`at`](../api/basic_json/at.md), [`operator[]`](../api/basic_json/operator%5B%5D.md),
@@ -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
`replace(pos, count, const StringType&)` is required.
- `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
[`operator[](const json_pointer&)`](../../api/basic_json/operator%5B%5D.md) to decide whether a reference token
denotes an array index.
+6 -3
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
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
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,8 +1105,6 @@ 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.
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
BSON stores the length of documents, arrays, strings, and binary values in a signed 32-bit integer, and MessagePack
+1 -1
View File
@@ -7003,6 +7003,7 @@ public:
switch (parent.m_data.m_type)
{
case value_t::null:
case value_t::object:
{
// use operator[] to add value
@@ -7034,7 +7035,6 @@ public:
// the parent of an "add" target must be an object or array
// (see #4292)
case value_t::null:
case value_t::string:
case value_t::boolean:
case value_t::number_integer:
+1 -1
View File
@@ -42793,6 +42793,7 @@ public:
switch (parent.m_data.m_type)
{
case value_t::null:
case value_t::object:
{
// use operator[] to add value
@@ -42824,7 +42825,6 @@ public:
// the parent of an "add" target must be an object or array
// (see #4292)
case value_t::null:
case value_t::string:
case value_t::boolean:
case value_t::number_integer:
+1 -223
View File
@@ -15,7 +15,6 @@ using nlohmann::json;
#endif
#include <fstream>
#include <limits>
#include <string>
#include <vector>
#include "make_test_data_available.hpp"
@@ -491,9 +490,7 @@ TEST_CASE("JSON patch")
}
// A.13. Invalid JSON Patch Document
// 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
// not applicable
SECTION("A.14. Escape Ordering")
{
@@ -1518,24 +1515,6 @@ TEST_CASE("JSON patch - add to a primitive parent (regression #4292)")
#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")
{
json const doc = {{"foo", 1}};
@@ -2124,204 +2103,3 @@ TEST_CASE_TEMPLATE("JSON patch - diff round trip on hand-made documents", Json,
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);
}
}
+50
View File
@@ -1044,6 +1044,56 @@ TEST_CASE("flatten of structured values")
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")
{
const json value =