mirror of
https://github.com/nlohmann/json.git
synced 2026-10-26 15:58:55 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab0307fb79 |
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -39,13 +39,33 @@ Basic guarantee: if an exception is thrown during the operation, the JSON value
|
||||
|
||||
## 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
|
||||
|
||||
`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
|
||||
`#!cpp *this`.
|
||||
- `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
|
||||
`#!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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
[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
|
||||
|
||||
The following code shows how a JSON Merge Patch is applied to a JSON document.
|
||||
|
||||
@@ -1100,8 +1100,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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Generated
+1
-1
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user