// __ _____ _____ _____ // __| | __| | | | 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()); } }