diff --git a/docs/mkdocs/docs/api/basic_json/merge_patch.md b/docs/mkdocs/docs/api/basic_json/merge_patch.md index 1fafb1ece..923070080 100644 --- a/docs/mkdocs/docs/api/basic_json/merge_patch.md +++ b/docs/mkdocs/docs/api/basic_json/merge_patch.md @@ -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 diff --git a/docs/mkdocs/docs/features/merge_patch.md b/docs/mkdocs/docs/features/merge_patch.md index ac732c7bf..9d87f4c50 100644 --- a/docs/mkdocs/docs/features/merge_patch.md +++ b/docs/mkdocs/docs/features/merge_patch.md @@ -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. diff --git a/tests/src/unit-merge_patch.cpp b/tests/src/unit-merge_patch.cpp index 927eb8773..3f5d79b3c 100644 --- a/tests/src/unit-merge_patch.cpp +++ b/tests/src/unit-merge_patch.cpp @@ -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() == 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()); + } +}