diff --git a/docs/mkdocs/docs/api/basic_json/diff.md b/docs/mkdocs/docs/api/basic_json/diff.md
index ae22454dd..c4e710a7d 100644
--- a/docs/mkdocs/docs/api/basic_json/diff.md
+++ b/docs/mkdocs/docs/api/basic_json/diff.md
@@ -59,6 +59,7 @@ Currently, only `remove`, `add`, and `replace` operations are generated.
- [patch](patch.md) applies a JSON Patch
- [patch_inplace](patch_inplace.md) applies a JSON Patch in place
- [merge_patch](merge_patch.md) applies a JSON Merge Patch
+- [merge_diff](merge_diff.md) creates a diff as a JSON Merge Patch
## Version history
diff --git a/docs/mkdocs/docs/api/basic_json/index.md b/docs/mkdocs/docs/api/basic_json/index.md
index fdb57105c..ee40bc2b0 100644
--- a/docs/mkdocs/docs/api/basic_json/index.md
+++ b/docs/mkdocs/docs/api/basic_json/index.md
@@ -285,6 +285,7 @@ Access to the JSON value
### JSON Merge Patch functions
- [**merge_patch**](merge_patch.md) - applies a JSON Merge Patch
+- [**merge_diff**](merge_diff.md) (_static_) - creates a diff as a JSON Merge Patch
## Static functions
diff --git a/docs/mkdocs/docs/api/basic_json/merge_diff.md b/docs/mkdocs/docs/api/basic_json/merge_diff.md
new file mode 100644
index 000000000..b606dbad0
--- /dev/null
+++ b/docs/mkdocs/docs/api/basic_json/merge_diff.md
@@ -0,0 +1,79 @@
+# nlohmann::basic_json::merge_diff
+
+```cpp
+static basic_json merge_diff(const basic_json& source,
+ const basic_json& target);
+```
+
+Creates a [JSON Merge Patch](https://tools.ietf.org/html/rfc7396) so that value `source` can be changed into the value
+`target` by calling the [`merge_patch`](merge_patch.md) function. The patch contains only what differs: changed and
+added members with their new values, and removed members with the value `#!json null`. Nested objects are compared
+member by member; any other value that differs, including an array, is replaced as a whole.
+
+For two JSON values `source` and `target`, where `target` contains no object member whose value is `#!json null`, the
+following code always yields `#!cpp true`:
+```cpp
+basic_json patched = source;
+patched.merge_patch(merge_diff(source, target));
+patched == target;
+```
+
+## Parameters
+
+`source` (in)
+: JSON value to compare from
+
+`target` (in)
+: JSON value to compare against
+
+## Return value
+
+a JSON Merge Patch to convert the `source` to `target`
+
+## Exception safety
+
+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, or adding one to the patch): 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)²).
+
+## Notes
+
+- A merge patch uses `#!json null` to remove a member, so it cannot set a member to `#!json null`. As in RFC 7396, a
+ `#!json null` member of `target` is treated as absent: the patch removes the member from `source` instead of setting
+ it to `#!json null`. A member that is `#!json null` in both `source` and `target` is unchanged and left out of the
+ patch, so `source` keeps it.
+- A merge patch cannot reorder object members, and [`merge_patch`](merge_patch.md) appends added members at the end.
+ For [`ordered_json`](../ordered_json.md), the patched value therefore has the same members as `target`, but not
+ necessarily in the same order, and may compare unequal to it. The patch lists changed and removed members in the
+ order of `source`, followed by added members in the order of `target`.
+
+## Examples
+
+??? example
+
+ The following code shows how a JSON Merge Patch is created as a diff for two JSON values.
+
+ ```cpp
+ --8<-- "examples/merge_diff.cpp"
+ ```
+
+ Output:
+
+ ```json
+ --8<-- "examples/merge_diff.output"
+ ```
+
+## See also
+
+- [RFC 7396 (JSON Merge Patch)](https://tools.ietf.org/html/rfc7396)
+- [merge_patch](merge_patch.md) applies a JSON Merge Patch
+- [diff](diff.md) creates a diff as a JSON Patch
+
+## Version history
+
+- Added in version 3.13.0.
diff --git a/docs/mkdocs/docs/api/basic_json/merge_patch.md b/docs/mkdocs/docs/api/basic_json/merge_patch.md
index dbda23f42..00e1bc8e5 100644
--- a/docs/mkdocs/docs/api/basic_json/merge_patch.md
+++ b/docs/mkdocs/docs/api/basic_json/merge_patch.md
@@ -67,6 +67,7 @@ returned by `#!cpp (*this)[key]`); it is read as it was when `merge_patch()` was
- [RFC 7396 (JSON Merge Patch)](https://tools.ietf.org/html/rfc7396)
- [patch](patch.md) apply a JSON patch
+- [merge_diff](merge_diff.md) creates a diff as a JSON Merge Patch
## Version history
diff --git a/docs/mkdocs/docs/examples/merge_diff.cpp b/docs/mkdocs/docs/examples/merge_diff.cpp
new file mode 100644
index 000000000..22ae9b087
--- /dev/null
+++ b/docs/mkdocs/docs/examples/merge_diff.cpp
@@ -0,0 +1,51 @@
+#include
+#include
+#include
+
+using json = nlohmann::json;
+using namespace nlohmann::literals;
+
+int main()
+{
+ // the source document
+ json source = R"(
+ {
+ "title": "Goodbye!",
+ "author": {
+ "givenName": "John",
+ "familyName": "Doe"
+ },
+ "tags": [
+ "example",
+ "sample"
+ ],
+ "content": "This will be unchanged"
+ }
+ )"_json;
+
+ // the target document
+ json target = R"(
+ {
+ "title": "Hello!",
+ "author": {
+ "givenName": "John"
+ },
+ "tags": [
+ "example"
+ ],
+ "content": "This will be unchanged",
+ "phoneNumber": "+01-123-456-7890"
+ }
+ )"_json;
+
+ // create the patch
+ json patch = json::merge_diff(source, target);
+
+ // roundtrip
+ json patched_source = source;
+ patched_source.merge_patch(patch);
+
+ // output patch and roundtrip result
+ std::cout << std::setw(4) << patch << "\n\n"
+ << std::setw(4) << patched_source << std::endl;
+}
diff --git a/docs/mkdocs/docs/examples/merge_diff.output b/docs/mkdocs/docs/examples/merge_diff.output
new file mode 100644
index 000000000..ca7c59bf2
--- /dev/null
+++ b/docs/mkdocs/docs/examples/merge_diff.output
@@ -0,0 +1,22 @@
+{
+ "author": {
+ "familyName": null
+ },
+ "phoneNumber": "+01-123-456-7890",
+ "tags": [
+ "example"
+ ],
+ "title": "Hello!"
+}
+
+{
+ "author": {
+ "givenName": "John"
+ },
+ "content": "This will be unchanged",
+ "phoneNumber": "+01-123-456-7890",
+ "tags": [
+ "example"
+ ],
+ "title": "Hello!"
+}
diff --git a/docs/mkdocs/docs/features/merge_patch.md b/docs/mkdocs/docs/features/merge_patch.md
index c0a316e65..396ea87dc 100644
--- a/docs/mkdocs/docs/features/merge_patch.md
+++ b/docs/mkdocs/docs/features/merge_patch.md
@@ -9,6 +9,9 @@ syntax that closely mimics the document being modified. Unlike [JSON Patch](json
express every kind of change (e.g., it cannot reorder array elements or remove a specific array element), but it is
easier to read and write for object-shaped documents.
+The function [`merge_diff`](../api/basic_json/merge_diff.md) creates a merge patch that changes one JSON value into
+another.
+
!!! tip "Not a general deep merge"
A merge patch is not a general deep merge: a `#!json null` value in the patch deletes the key from the target.
@@ -35,4 +38,5 @@ easier to read and write for object-shaped documents.
- [JSON Patch and Diff](json_patch.md) - a more expressive alternative that describes a sequence of operations
- [JSON Pointer](json_pointer.md) - the addressing scheme used by JSON Patch
- Function [`merge_patch`](../api/basic_json/merge_patch.md)
+- Function [`merge_diff`](../api/basic_json/merge_diff.md)
- Function [`update`](../api/basic_json/update.md) - merge objects, optionally recursively
diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml
index 111e0da81..74b6c4d46 100644
--- a/docs/mkdocs/mkdocs.yml
+++ b/docs/mkdocs/mkdocs.yml
@@ -182,6 +182,7 @@ nav:
- 'json_serializer': api/basic_json/json_serializer.md
- 'max_size': api/basic_json/max_size.md
- 'meta': api/basic_json/meta.md
+ - 'merge_diff': api/basic_json/merge_diff.md
- 'merge_patch': api/basic_json/merge_patch.md
- 'number_float_t': api/basic_json/number_float_t.md
- 'number_integer_t': api/basic_json/number_integer_t.md
diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp
index b91fa3e27..fae89829a 100644
--- a/include/nlohmann/json.hpp
+++ b/include/nlohmann/json.hpp
@@ -7449,6 +7449,176 @@ public:
}
public:
+ /// @brief creates a diff as a JSON Merge Patch
+ /// @sa https://json.nlohmann.me/api/basic_json/merge_diff/
+ JSON_HEDLEY_WARN_UNUSED_RESULT
+ static basic_json merge_diff(const basic_json& source, const basic_json& target)
+ {
+ // unless both are objects, the patch is the target itself
+ if (!source.is_object() || !target.is_object())
+ {
+ return target;
+ }
+
+ return merge_diff_recursively(source, target, 0);
+ }
+
+ private:
+ /// @brief two objects @ref merge_diff_iteratively is diffing
+ struct merge_diff_frame
+ {
+ merge_diff_frame(const basic_json* source_, const basic_json* target_)
+ : source(source_), target(target_), member(source_->cbegin()), patch(value_t::object)
+ {}
+
+ // declared for GCC's -Weffc++, as in diff_frame
+ merge_diff_frame(const merge_diff_frame&) = default;
+ merge_diff_frame(merge_diff_frame&&) = default;
+ merge_diff_frame& operator=(const merge_diff_frame&) = default;
+ merge_diff_frame& operator=(merge_diff_frame&&) = default;
+ ~merge_diff_frame() = default;
+
+ /// the objects being diffed
+ const basic_json* source;
+ const basic_json* target;
+ /// the member of source being diffed; advanced once it is done
+ const_iterator member;
+ /// the patch for the members of source diffed so far
+ basic_json patch;
+ };
+
+ /// @brief add the members of object @a target that @a source does not
+ /// have to the merge patch @a result
+ static void merge_diff_added_members(basic_json& result, const basic_json& source, const basic_json& target)
+ {
+ for (auto it = target.begin(); it != target.end(); ++it)
+ {
+ auto itf = source.find(it.key());
+ if (itf == source.end())
+ {
+ result[it.key()] = it.value();
+ }
+ }
+ }
+
+ /*!
+ @brief @ref merge_diff of two objects at nesting level @a depth
+
+ Diffing two nested objects calls this function again, once per nesting
+ level, so values nested deeply enough would exhaust the call stack and
+ terminate the process. The descent is bounded here: once @ref
+ detail::recursion_depth_limit levels have been entered, @ref
+ merge_diff_iteratively diffs what is left without the call stack.
+ */
+ static basic_json merge_diff_recursively(const basic_json& source, const basic_json& target, const std::size_t depth)
+ {
+ JSON_ASSERT(source.is_object() && target.is_object());
+
+ if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit()))
+ {
+ return merge_diff_iteratively(source, target);
+ }
+
+ basic_json result(value_t::object);
+
+ for (auto it = source.begin(); it != source.end(); ++it)
+ {
+ auto itf = target.find(it.key());
+ if (itf != target.end())
+ {
+ if (it.value().is_object() && itf.value().is_object())
+ {
+ auto diff = merge_diff_recursively(it.value(), itf.value(), depth + 1);
+ if (!diff.empty())
+ {
+ result[it.key()] = std::move(diff);
+ }
+ }
+ else if (it.value() != itf.value())
+ {
+ result[it.key()] = itf.value();
+ }
+ }
+ else
+ {
+ result[it.key()] = value_t::null;
+ }
+ }
+
+ merge_diff_added_members(result, source, target);
+ return result;
+ }
+
+ /*!
+ @brief @ref merge_diff of two objects without the call stack
+
+ Produces the same patch as @ref merge_diff_recursively, keeping the objects
+ being diffed on an explicit stack instead of descending into them. Two
+ nested objects are still diffed completely before the next member, so the
+ patch has its members in the same order as in the recursive version. Only
+ reached for objects nested more deeply than @ref
+ detail::recursion_depth_limit.
+ */
+ static basic_json merge_diff_iteratively(const basic_json& source, const basic_json& target)
+ {
+ // the patch of source and target, set once their frame is popped
+ basic_json result;
+ std::vector stack;
+ stack.emplace_back(&source, &target);
+ while (!stack.empty())
+ {
+ // the frame's members are copied out and it is changed through
+ // stack.back(): pushing a frame for nested objects or popping
+ // this one would invalidate a reference to it
+ const basic_json* const s = stack.back().source;
+ const basic_json* const t = stack.back().target;
+ const const_iterator it = stack.back().member;
+
+ if (it != s->cend())
+ {
+ const const_iterator itf = t->find(it.key());
+ if (itf != t->cend())
+ {
+ if (it.value().is_object() && itf.value().is_object())
+ {
+ // member is advanced once their patch is under its key
+ stack.emplace_back(&it.value(), &itf.value());
+ continue;
+ }
+ if (it.value() != itf.value())
+ {
+ stack.back().patch[it.key()] = itf.value();
+ }
+ }
+ else
+ {
+ stack.back().patch[it.key()] = value_t::null;
+ }
+ ++stack.back().member;
+ continue;
+ }
+
+ merge_diff_added_members(stack.back().patch, *s, *t);
+
+ // these objects are done: continue with the ones they are in
+ basic_json patch = std::move(stack.back().patch);
+ stack.pop_back();
+ if (stack.empty())
+ {
+ result = std::move(patch);
+ }
+ else
+ {
+ if (!patch.empty())
+ {
+ stack.back().patch[stack.back().member.key()] = std::move(patch);
+ }
+ ++stack.back().member;
+ }
+ }
+ return result;
+ }
+
/// @}
};
diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp
index d8f7e41b3..b6b927835 100644
--- a/single_include/nlohmann/json.hpp
+++ b/single_include/nlohmann/json.hpp
@@ -35570,6 +35570,176 @@ public:
}
public:
+ /// @brief creates a diff as a JSON Merge Patch
+ /// @sa https://json.nlohmann.me/api/basic_json/merge_diff/
+ JSON_HEDLEY_WARN_UNUSED_RESULT
+ static basic_json merge_diff(const basic_json& source, const basic_json& target)
+ {
+ // unless both are objects, the patch is the target itself
+ if (!source.is_object() || !target.is_object())
+ {
+ return target;
+ }
+
+ return merge_diff_recursively(source, target, 0);
+ }
+
+ private:
+ /// @brief two objects @ref merge_diff_iteratively is diffing
+ struct merge_diff_frame
+ {
+ merge_diff_frame(const basic_json* source_, const basic_json* target_)
+ : source(source_), target(target_), member(source_->cbegin()), patch(value_t::object)
+ {}
+
+ // declared for GCC's -Weffc++, as in diff_frame
+ merge_diff_frame(const merge_diff_frame&) = default;
+ merge_diff_frame(merge_diff_frame&&) = default;
+ merge_diff_frame& operator=(const merge_diff_frame&) = default;
+ merge_diff_frame& operator=(merge_diff_frame&&) = default;
+ ~merge_diff_frame() = default;
+
+ /// the objects being diffed
+ const basic_json* source;
+ const basic_json* target;
+ /// the member of source being diffed; advanced once it is done
+ const_iterator member;
+ /// the patch for the members of source diffed so far
+ basic_json patch;
+ };
+
+ /// @brief add the members of object @a target that @a source does not
+ /// have to the merge patch @a result
+ static void merge_diff_added_members(basic_json& result, const basic_json& source, const basic_json& target)
+ {
+ for (auto it = target.begin(); it != target.end(); ++it)
+ {
+ auto itf = source.find(it.key());
+ if (itf == source.end())
+ {
+ result[it.key()] = it.value();
+ }
+ }
+ }
+
+ /*!
+ @brief @ref merge_diff of two objects at nesting level @a depth
+
+ Diffing two nested objects calls this function again, once per nesting
+ level, so values nested deeply enough would exhaust the call stack and
+ terminate the process. The descent is bounded here: once @ref
+ detail::recursion_depth_limit levels have been entered, @ref
+ merge_diff_iteratively diffs what is left without the call stack.
+ */
+ static basic_json merge_diff_recursively(const basic_json& source, const basic_json& target, const std::size_t depth)
+ {
+ JSON_ASSERT(source.is_object() && target.is_object());
+
+ if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit()))
+ {
+ return merge_diff_iteratively(source, target);
+ }
+
+ basic_json result(value_t::object);
+
+ for (auto it = source.begin(); it != source.end(); ++it)
+ {
+ auto itf = target.find(it.key());
+ if (itf != target.end())
+ {
+ if (it.value().is_object() && itf.value().is_object())
+ {
+ auto diff = merge_diff_recursively(it.value(), itf.value(), depth + 1);
+ if (!diff.empty())
+ {
+ result[it.key()] = std::move(diff);
+ }
+ }
+ else if (it.value() != itf.value())
+ {
+ result[it.key()] = itf.value();
+ }
+ }
+ else
+ {
+ result[it.key()] = value_t::null;
+ }
+ }
+
+ merge_diff_added_members(result, source, target);
+ return result;
+ }
+
+ /*!
+ @brief @ref merge_diff of two objects without the call stack
+
+ Produces the same patch as @ref merge_diff_recursively, keeping the objects
+ being diffed on an explicit stack instead of descending into them. Two
+ nested objects are still diffed completely before the next member, so the
+ patch has its members in the same order as in the recursive version. Only
+ reached for objects nested more deeply than @ref
+ detail::recursion_depth_limit.
+ */
+ static basic_json merge_diff_iteratively(const basic_json& source, const basic_json& target)
+ {
+ // the patch of source and target, set once their frame is popped
+ basic_json result;
+ std::vector stack;
+ stack.emplace_back(&source, &target);
+ while (!stack.empty())
+ {
+ // the frame's members are copied out and it is changed through
+ // stack.back(): pushing a frame for nested objects or popping
+ // this one would invalidate a reference to it
+ const basic_json* const s = stack.back().source;
+ const basic_json* const t = stack.back().target;
+ const const_iterator it = stack.back().member;
+
+ if (it != s->cend())
+ {
+ const const_iterator itf = t->find(it.key());
+ if (itf != t->cend())
+ {
+ if (it.value().is_object() && itf.value().is_object())
+ {
+ // member is advanced once their patch is under its key
+ stack.emplace_back(&it.value(), &itf.value());
+ continue;
+ }
+ if (it.value() != itf.value())
+ {
+ stack.back().patch[it.key()] = itf.value();
+ }
+ }
+ else
+ {
+ stack.back().patch[it.key()] = value_t::null;
+ }
+ ++stack.back().member;
+ continue;
+ }
+
+ merge_diff_added_members(stack.back().patch, *s, *t);
+
+ // these objects are done: continue with the ones they are in
+ basic_json patch = std::move(stack.back().patch);
+ stack.pop_back();
+ if (stack.empty())
+ {
+ result = std::move(patch);
+ }
+ else
+ {
+ if (!patch.empty())
+ {
+ stack.back().patch[stack.back().member.key()] = std::move(patch);
+ }
+ ++stack.back().member;
+ }
+ }
+ return result;
+ }
+
/// @}
};
diff --git a/tests/src/unit-diagnostics.cpp b/tests/src/unit-diagnostics.cpp
index 1ada023b7..37427587c 100644
--- a/tests/src/unit-diagnostics.cpp
+++ b/tests/src/unit-diagnostics.cpp
@@ -567,9 +567,9 @@ TEST_CASE("Regression tests for extended diagnostics")
}
}
-TEST_CASE("Better diagnostics past the descent bound of update() and merge_patch()")
+TEST_CASE("Better diagnostics past the descent bound of update(), merge_patch() and merge_diff()")
{
- // Both merge objects nested more than detail::recursion_depth_limit()
+ // All three handle objects nested more than detail::recursion_depth_limit()
// (128) levels deep without recursing; the values they add or replace
// there must still know their parents.
// The values are built rather than parsed, so that the expected messages
@@ -615,4 +615,19 @@ TEST_CASE("Better diagnostics past the descent bound of update() and merge_patch
CHECK_THROWS_WITH_AS(p->at("x").at(0), expected_x.c_str(), json::type_error);
CHECK_THROWS_WITH_AS(p->at("y").at(0), expected_y.c_str(), json::type_error);
}
+
+ SECTION("merge_diff()")
+ {
+ // target and patch differ only in their innermost members, so the
+ // diff of the two is nested as deeply: {"a": ... {"x": null, "y": 2}}
+ const json diff = json::merge_diff(target, patch); // NOLINT(readability-suspicious-call-argument)
+
+ const json* p = &diff;
+ for (std::size_t i = 0; i < depth; ++i)
+ {
+ p = &p->at("a");
+ }
+ CHECK(p->at("x").is_null());
+ CHECK_THROWS_WITH_AS(p->at("y").at(0), expected_y.c_str(), json::type_error);
+ }
}
diff --git a/tests/src/unit-merge_diff.cpp b/tests/src/unit-merge_diff.cpp
new file mode 100644
index 000000000..77fa58768
--- /dev/null
+++ b/tests/src/unit-merge_diff.cpp
@@ -0,0 +1,1024 @@
+// __ _____ _____ _____
+// __| | __| | | | JSON for Modern C++ (supporting code)
+// | | |__ | | | | | | version 3.12.0
+// |_____|_____|_____|_|___| https://github.com/nlohmann/json
+//
+// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann
+// SPDX-License-Identifier: MIT
+
+#include "doctest_compatibility.h"
+
+#include
+using nlohmann::json;
+using nlohmann::ordered_json;
+#ifdef JSON_TEST_NO_GLOBAL_UDLS
+ using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
+#endif
+
+#include
+#include
+
+namespace
+{
+// merge_diff() recursing once per nesting level, without the bound on its
+// descent; only usable on values nested a few hundred levels deep
+template
+BasicJsonType reference_merge_diff(const BasicJsonType& source, const BasicJsonType& target)
+{
+ if (!source.is_object() || !target.is_object())
+ {
+ return target;
+ }
+
+ BasicJsonType result = BasicJsonType::object();
+ for (auto it = source.begin(); it != source.end(); ++it)
+ {
+ const auto itf = target.find(it.key());
+ if (itf == target.end())
+ {
+ result[it.key()] = nullptr;
+ }
+ else if (it.value().is_object() && itf.value().is_object())
+ {
+ auto diff = reference_merge_diff(it.value(), itf.value());
+ if (!diff.empty())
+ {
+ result[it.key()] = std::move(diff);
+ }
+ }
+ else if (it.value() != itf.value())
+ {
+ result[it.key()] = itf.value();
+ }
+ }
+ for (auto it = target.begin(); it != target.end(); ++it)
+ {
+ if (source.find(it.key()) == source.end())
+ {
+ result[it.key()] = it.value();
+ }
+ }
+ return result;
+}
+
+// objects nested `depth` levels deep under the key "a", with members before
+// and after it that differ by `variant` on the way down, so that diffing two
+// variants removes, adds and replaces members on many levels, and diffs nested
+// objects that are equal, that differ, or that are not objects in one of the
+// variants; only variant 2 has members that are null
+template
+BasicJsonType nested_objects(const std::size_t depth, const int variant)
+{
+ BasicJsonType value = BasicJsonType::object({{"v", variant}});
+ for (std::size_t i = 0; i < depth; ++i)
+ {
+ BasicJsonType object = BasicJsonType::object();
+ if ((i + static_cast(variant)) % 3 == 0)
+ {
+ object["s" + std::to_string(variant)] = i;
+ }
+ object["c"] = i % 4 == 0 ? variant : 0;
+ if (i % 5 == 0)
+ {
+ if (variant == 0)
+ {
+ object["o"] = BasicJsonType::object({{"p", i}});
+ }
+ else if (variant == 1)
+ {
+ object["o"] = BasicJsonType::array({i});
+ }
+ else
+ {
+ object["o"] = nullptr;
+ }
+ }
+ if (variant == 2 && i % 4 == 2)
+ {
+ object["n"] = nullptr;
+ }
+ object["a"] = std::move(value);
+ if (i % 6 == 0)
+ {
+ object["d"] = BasicJsonType::object({{"e", variant == 1 ? 1 : 0}});
+ }
+ if (variant == 1 && i % 7 == 0)
+ {
+ object["z"] = "z";
+ }
+ value = std::move(object);
+ }
+ return value;
+}
+} // namespace
+
+TEST_CASE("JSON Merge Patch diff")
+{
+ SECTION("examples from RFC 7396")
+ {
+ SECTION("Section 1")
+ {
+ json document = R"({
+ "a": "b",
+ "c": {
+ "d": "e",
+ "f": "g"
+ }
+ })"_json;
+
+ json expected = R"({
+ "a": "z",
+ "c": {
+ "d": "e"
+ }
+ })"_json;
+
+ json const expected_patch = R"({
+ "a": "z",
+ "c": {
+ "f": null
+ }
+ })"_json;
+
+ auto patch = json::merge_diff(document, expected);
+ CHECK(patch == expected_patch);
+
+ document.merge_patch(patch);
+ CHECK(document == expected);
+ }
+
+ SECTION("Section 3")
+ {
+ json document = R"({
+ "title": "Goodbye!",
+ "author": {
+ "givenName": "John",
+ "familyName": "Doe"
+ },
+ "tags": [
+ "example",
+ "sample"
+ ],
+ "content": "This will be unchanged"
+ })"_json;
+
+ json expected = R"({
+ "title": "Hello!",
+ "author": {
+ "givenName": "John"
+ },
+ "tags": [
+ "example"
+ ],
+ "content": "This will be unchanged",
+ "phoneNumber": "+01-123-456-7890"
+ })"_json;
+
+ json const expected_patch = R"({
+ "title": "Hello!",
+ "phoneNumber": "+01-123-456-7890",
+ "author": {
+ "familyName": null
+ },
+ "tags": [
+ "example"
+ ]
+ })"_json;
+
+ auto patch = json::merge_diff(document, expected);
+ CHECK(patch == expected_patch);
+
+ document.merge_patch(patch);
+ CHECK(document == expected);
+ }
+
+ SECTION("Appendix A")
+ {
+ SECTION("Example 1")
+ {
+ json original = R"({"a":"b"})"_json;
+ json result = R"({"a":"c"})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":"c"})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("Example 2")
+ {
+ json original = R"({"a":"b"})"_json;
+ json result = R"({"a":"b", "b":"c"})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"b":"c"})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("Example 3")
+ {
+ json original = R"({"a":"b"})"_json;
+ json result = R"({})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":null})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("Example 4")
+ {
+ json original = R"({"a":"b","b":"c"})"_json;
+ json result = R"({"b":"c"})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":null})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("Example 5")
+ {
+ json original = R"({"a":["b"]})"_json;
+ json result = R"({"a":"c"})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":"c"})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("Example 6")
+ {
+ json original = R"({"a":"c"})"_json;
+ json result = R"({"a":["b"]})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":["b"]})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("Example 7")
+ {
+ json original = R"({"a":{"b": "c"}})"_json;
+ json result = R"({"a": {"b": "d"}})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ // differs from the RFC 7396, "c": null is not in the patch
+ // because neither the source nor the target have the "c" key
+ CHECK(patch == R"({"a":{"b":"d"}})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("Example 8")
+ {
+ json original = R"({"a":[{"b":"c"}]})"_json;
+ json result = R"({"a":[1]})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":[1]})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("Example 9")
+ {
+ json original = R"(["a","b"])"_json;
+ json result = R"(["c","d"])"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"(["c","d"])"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("Example 10")
+ {
+ json original = R"({"a":"b"})"_json;
+ json result = R"(["c"])"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"(["c"])"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("Example 11")
+ {
+ json original = R"({"a":"foo"})"_json;
+ json result = R"(null)"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"(null)"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("Example 12")
+ {
+ json original = R"({"a":"foo"})"_json;
+ json result = R"("bar")"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"("bar")"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("Example 13")
+ {
+ json original = R"({"e":null})"_json;
+ json result = R"({"e":null,"a":1})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":1})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("Example 14")
+ {
+ json original = R"([1,2])"_json;
+ json result = R"({"a":"b"})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ // differs from the RFC 7396, "c": null is not in the patch
+ // because neither the source nor the target have the "c" key
+ CHECK(patch == R"({"a":"b"})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("Example 15")
+ {
+ json original = R"({})"_json;
+ json result = R"({"a":{"bb":{}}})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ // differs from the RFC 7396, "ccc": null is not in the patch
+ // because neither the source nor the target have the "ccc" key
+ CHECK(patch == R"({"a":{"bb":{}}})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+ }
+ }
+
+ SECTION("null values")
+ {
+ SECTION("object with null value to object")
+ {
+ json original = R"({"a":null})"_json;
+ json result = R"({"a":{"b":"c"}})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":{"b":"c"}})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == R"({"a":{"b":"c"}})"_json);
+ }
+
+ SECTION("object to object with null value")
+ {
+ json original = R"({"a":{"b":"c"}})"_json;
+ json result = R"({"a":null})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":null})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == R"({})"_json);
+ }
+
+ SECTION("primitive to object with null value")
+ {
+ json original = R"({"a":1})"_json;
+ json result = R"({"a":null})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":null})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == R"({})"_json);
+ }
+
+ SECTION("nested primitive to object with null value")
+ {
+ json original = R"({"a":{"b":1}})"_json;
+ json result = R"({"a":{"b":null}})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":{"b":null}})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == R"({"a":{}})"_json);
+ }
+
+ SECTION("array value to object with null value")
+ {
+ json original = R"({"a":[1,2]})"_json;
+ json result = R"({"a":null})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":null})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == R"({})"_json);
+ }
+
+ SECTION("nested object to object with null value")
+ {
+ json original = R"({"a":{"b":{"c":"d"}}})"_json;
+ json result = R"({"a":{"b":null}})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":{"b":null}})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == R"({"a":{}})"_json);
+ }
+
+ SECTION("nested primitive to object with null value with sibling")
+ {
+ json original = R"({"x":"y","a":{"b":"c"}})"_json;
+ json result = R"({"x":"y","a":{"b":null, "c":"d"}})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":{"b":null,"c":"d"}})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == R"({"x":"y","a":{"c":"d"}})"_json);
+ }
+
+ SECTION("empty object to object with null value")
+ {
+ json original = R"({})"_json;
+ json result = R"({"a":null})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":null})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == R"({})"_json);
+ }
+
+ SECTION("null to object with null value")
+ {
+ json original = R"(null)"_json;
+ json result = R"({"a":null})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":null})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == R"({})"_json);
+ }
+
+ SECTION("array to object with null value")
+ {
+ json original = R"([])"_json;
+ json result = R"({"a":null})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":null})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == R"({})"_json);
+ }
+
+ SECTION("empty object to nested null value")
+ {
+ json original = R"({})"_json;
+ json result = R"({"a":{"b":null}})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":{"b":null}})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == R"({"a":{}})"_json);
+ }
+
+ SECTION("object to nested null value")
+ {
+ json original = R"({"a": 1})"_json;
+ json result = R"({"a":{"b":null}})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":{"b":null}})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == R"({"a":{}})"_json);
+ }
+
+ SECTION("primitive to nested null value")
+ {
+ json original = R"(1)"_json;
+ json result = R"({"a":{"b":null}})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":{"b":null}})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == R"({"a":{}})"_json);
+ }
+
+ SECTION("object with null value unchanged")
+ {
+ json original = R"({"a":null})"_json;
+ json result = R"({"a":null})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("object with null value to empty object")
+ {
+ json original = R"({"a":null})"_json;
+ json result = R"({})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":null})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("object with null value to primitive")
+ {
+ json original = R"({"a":null})"_json;
+ json result = R"({"a":1})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":1})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("array to array with null value")
+ {
+ json original = R"({"a":[1]})"_json;
+ json result = R"({"a":[null]})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":[null]})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("empty object to array with null value")
+ {
+ json original = R"({})"_json;
+ json result = R"({"a":[null]})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":[null]})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("doubly nested primitive to null value")
+ {
+ json original = R"({"a":{"b":{"c":1}}})"_json;
+ json result = R"({"a":{"b":{"c":null}}})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":{"b":{"c":null}}})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == R"({"a":{"b":{}}})"_json);
+ }
+ }
+
+ SECTION("no change")
+ {
+ SECTION("object")
+ {
+ json original = R"({"a":"b","b":"c"})"_json;
+ json result = R"({"a":"b","b":"c"})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch.empty());
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("empty object")
+ {
+ json original = R"({})"_json;
+ json result = R"({})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch.empty());
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("array")
+ {
+ json original = R"([1,2,3])"_json;
+ json result = R"([1,2,3])"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(!patch.empty());
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("null")
+ {
+ json original = R"(null)"_json;
+ json result = R"(null)"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch.empty());
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("string")
+ {
+ json original = R"("ab")"_json;
+ json result = R"("ab")"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(!patch.empty());
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("number")
+ {
+ json original = R"(42)"_json;
+ json result = R"(42)"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(!patch.empty());
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+ }
+
+ SECTION("primitives")
+ {
+ SECTION("string")
+ {
+ json original = R"("a")"_json;
+ json result = R"("b")"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"("b")"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("number")
+ {
+ json original = R"(1)"_json;
+ json result = R"(2)"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"(2)"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("boolean")
+ {
+ json original = R"(false)"_json;
+ json result = R"(true)"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"(true)"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("object to primitive")
+ {
+ json original = R"({"a":{"b":"c"}})"_json;
+ json result = R"({"a":1})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":1})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("primitive to object")
+ {
+ json original = R"({"a":1})"_json;
+ json result = R"({"a":{"b":"c"}})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":{"b":"c"}})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("object to empty string")
+ {
+ json original = R"({"a":{"b":"c"}})"_json;
+ json result = R"({"a":""})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":""})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("object to zero")
+ {
+ json original = R"({"a":{"b":"c"}})"_json;
+ json result = R"({"a":0})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":0})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("object to empty object")
+ {
+ json original = R"({"a":{"b":"c"}})"_json;
+ json result = R"({"a":{}})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":{"b":null}})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+ }
+
+ SECTION("arrays")
+ {
+ SECTION("array to array")
+ {
+ json original = R"([1,2,3])"_json;
+ json result = R"([1,2,4])"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"([1,2,4])"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("array to empty array")
+ {
+ json original = R"([1,2,3])"_json;
+ json result = R"([])"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"([])"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("empty array to array")
+ {
+ json original = R"([])"_json;
+ json result = R"([1,2,3])"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"([1,2,3])"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("object to empty array")
+ {
+ json original = R"({"a":{"b":"c"}})"_json;
+ json result = R"({"a":[]})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":[]})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("same keys")
+ {
+ json original = R"(["a","b"])"_json;
+ json result = R"({"a":1,"b":2})"_json;
+
+ auto patch = json::merge_diff(original, result);
+ CHECK(patch == R"({"a":1,"b":2})"_json);
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+ }
+
+ SECTION("ordered_json")
+ {
+ SECTION("changed value keeps member order")
+ {
+ auto original = ordered_json::parse(R"({"c":1,"b":2,"a":3})");
+ auto result = ordered_json::parse(R"({"c":1,"b":5,"a":3})");
+
+ auto patch = ordered_json::merge_diff(original, result);
+ CHECK(patch.dump() == R"({"b":5})");
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ CHECK(original.dump() == R"({"c":1,"b":5,"a":3})");
+ }
+
+ SECTION("added members follow target order")
+ {
+ auto original = ordered_json::parse(R"({"b":1})");
+ auto result = ordered_json::parse(R"({"b":1,"z":2,"a":3})");
+
+ auto patch = ordered_json::merge_diff(original, result);
+ CHECK(patch.dump() == R"({"z":2,"a":3})");
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ CHECK(original.dump() == R"({"b":1,"z":2,"a":3})");
+ }
+
+ SECTION("removed members follow source order")
+ {
+ auto original = ordered_json::parse(R"({"c":1,"b":2,"a":3})");
+ auto result = ordered_json::parse(R"({"b":2})");
+
+ auto patch = ordered_json::merge_diff(original, result);
+ CHECK(patch.dump() == R"({"c":null,"a":null})");
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ }
+
+ SECTION("nested objects keep member order")
+ {
+ auto original = ordered_json::parse(R"({"z":{"y":1,"x":2},"a":0})");
+ auto result = ordered_json::parse(R"({"z":{"y":1,"x":3,"w":4},"a":0})");
+
+ auto patch = ordered_json::merge_diff(original, result);
+ CHECK(patch.dump() == R"({"z":{"x":3,"w":4}})");
+
+ original.merge_patch(patch);
+ CHECK(original == result);
+ CHECK(original.dump() == R"({"z":{"y":1,"x":3,"w":4},"a":0})");
+ }
+
+ SECTION("reordered members cannot be expressed")
+ {
+ auto original = ordered_json::parse(R"({"a":1,"b":2})");
+ auto result = ordered_json::parse(R"({"b":2,"a":1})");
+
+ auto patch = ordered_json::merge_diff(original, result);
+ CHECK(patch.empty());
+
+ original.merge_patch(patch);
+ CHECK(original != result);
+ CHECK(original.dump() == R"({"a":1,"b":2})");
+ }
+
+ SECTION("member added before existing ones is appended")
+ {
+ auto original = ordered_json::parse(R"({"b":1})");
+ auto result = ordered_json::parse(R"({"a":0,"b":1})");
+
+ auto patch = ordered_json::merge_diff(original, result);
+ CHECK(patch.dump() == R"({"a":0})");
+
+ original.merge_patch(patch);
+ CHECK(original != result);
+ CHECK(original.dump() == R"({"b":1,"a":0})");
+ }
+ }
+}
+
+TEST_CASE("JSON Merge Patch diff of deeply nested values")
+{
+ SECTION("the patch is the same past the descent bound")
+ {
+ // depths on either side of the nesting depth up to which merge_diff()
+ // recurses (detail::recursion_depth_limit(), 128); not every depth up
+ // to 300, to keep the test fast under Valgrind
+ std::vector depths;
+ for (std::size_t depth = 0; depth <= 16; ++depth)
+ {
+ depths.push_back(depth);
+ }
+ for (std::size_t depth = 120; depth <= 136; ++depth)
+ {
+ depths.push_back(depth);
+ }
+ depths.push_back(300);
+
+ for (const auto depth : depths)
+ {
+ CAPTURE(depth)
+ for (int from = 0; from < 3; ++from)
+ {
+ for (int to = 0; to < 3; ++to)
+ {
+ CAPTURE(from)
+ CAPTURE(to)
+ const auto source = nested_objects(depth, from);
+ const auto target = nested_objects(depth, to);
+ const auto patch = json::merge_diff(source, target);
+ CHECK(patch == reference_merge_diff(source, target));
+ CHECK(patch.empty() == (from == to));
+
+ // a merge patch cannot set a member to null, so it only
+ // reproduces a target without null members
+ if (to != 2)
+ {
+ json patched = source;
+ patched.merge_patch(patch);
+ CHECK(patched == target);
+ }
+
+ // the members of the patch also come in the same order
+ const auto ordered_source = nested_objects(depth, from);
+ const auto ordered_target = nested_objects(depth, to);
+ CHECK(ordered_json::merge_diff(ordered_source, ordered_target).dump() == reference_merge_diff(ordered_source, ordered_target).dump());
+ }
+ }
+ }
+ }
+
+ SECTION("a difference only in the innermost value is a patch nested as deeply")
+ {
+ for (std::size_t depth = 0; depth <= 300; ++depth)
+ {
+ CAPTURE(depth)
+ json source = {{"v", 1}};
+ json target = {{"v", 2}};
+ json expected = {{"v", 2}};
+ for (std::size_t i = 0; i < depth; ++i)
+ {
+ source = json{{"a", std::move(source)}, {"b", i}};
+ target = json{{"a", std::move(target)}, {"b", i}};
+ expected = json{{"a", std::move(expected)}};
+ }
+ CHECK(json::merge_diff(source, target) == expected);
+ }
+ }
+
+ SECTION("values nested too deeply for the call stack")
+ {
+ // recursing once per nesting level would exhaust the call stack here.
+ // The patch is walked rather than compared as a whole, to keep the
+ // test fast.
+ const std::size_t depth = 100000;
+ std::string opening;
+ for (std::size_t i = 0; i < depth; ++i)
+ {
+ opening += R"({"a":)";
+ }
+ const std::string closing(depth, '}');
+ const auto source = json::parse(opening + R"({"v":1,"w":0})" + closing);
+
+ const auto patch = json::merge_diff(source, json::parse(opening + R"({"v":2,"w":0})" + closing));
+ std::size_t members = 0;
+ const json* p = &patch;
+ for (std::size_t i = 0; i < depth; ++i)
+ {
+ members += p->size();
+ p = &p->at("a");
+ }
+ CHECK(members == depth);
+ CHECK(*p == json({{"v", 2}}));
+
+ CHECK(json::merge_diff(source, json::parse(opening + R"({"v":1,"w":0})" + closing)).empty());
+ }
+}