mirror of
https://github.com/nlohmann/json.git
synced 2026-10-27 16:28:57 +00:00
509 lines
16 KiB
C++
509 lines
16 KiB
C++
// __ _____ _____ _____
|
|
// __| | __| | | | JSON for Modern C++ (supporting code)
|
|
// | | |__ | | | | | | version 3.12.0
|
|
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
|
//
|
|
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#include "doctest_compatibility.h"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
using nlohmann::json;
|
|
#ifdef JSON_TEST_NO_GLOBAL_UDLS
|
|
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
|
|
#endif
|
|
|
|
#include <string>
|
|
|
|
namespace
|
|
{
|
|
// RFC 7396's MergePatch, written recursively as in the RFC; only usable on
|
|
// values nested a few hundred levels deep
|
|
void reference_merge_patch(json& target, const json& patch)
|
|
{
|
|
if (!patch.is_object())
|
|
{
|
|
target = patch;
|
|
return;
|
|
}
|
|
if (!target.is_object())
|
|
{
|
|
target = json::object();
|
|
}
|
|
for (auto it = patch.begin(); it != patch.end(); ++it)
|
|
{
|
|
if (it.value().is_null())
|
|
{
|
|
target.erase(it.key());
|
|
}
|
|
else
|
|
{
|
|
reference_merge_patch(target[it.key()], it.value());
|
|
}
|
|
}
|
|
}
|
|
|
|
// objects nested `depth` levels deep under the key "a", with members that
|
|
// differ by `variant` on the way down
|
|
std::string nested_objects(const std::size_t depth, const int variant)
|
|
{
|
|
std::string text;
|
|
for (std::size_t i = 0; i < depth; ++i)
|
|
{
|
|
text += "{";
|
|
if ((i + static_cast<std::size_t>(variant)) % 3 == 0)
|
|
{
|
|
text += "\"s" + std::to_string(variant) + "\":" + std::to_string(i) + ",";
|
|
}
|
|
if (variant == 2 && i % 5 == 0)
|
|
{
|
|
text += "\"s0\":null,";
|
|
}
|
|
text += "\"a\":";
|
|
}
|
|
text += variant == 1 ? R"({"x":1,"y":null})" : "{\"y\":2}";
|
|
text.append(depth, '}');
|
|
return text;
|
|
}
|
|
} // namespace
|
|
|
|
TEST_CASE("JSON Merge Patch")
|
|
{
|
|
SECTION("examples from RFC 7396")
|
|
{
|
|
SECTION("Section 1")
|
|
{
|
|
json document = R"({
|
|
"a": "b",
|
|
"c": {
|
|
"d": "e",
|
|
"f": "g"
|
|
}
|
|
})"_json;
|
|
|
|
json const patch = R"({
|
|
"a": "z",
|
|
"c": {
|
|
"f": null
|
|
}
|
|
})"_json;
|
|
|
|
json expected = R"({
|
|
"a": "z",
|
|
"c": {
|
|
"d": "e"
|
|
}
|
|
})"_json;
|
|
|
|
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 const patch = R"({
|
|
"title": "Hello!",
|
|
"phoneNumber": "+01-123-456-7890",
|
|
"author": {
|
|
"familyName": null
|
|
},
|
|
"tags": [
|
|
"example"
|
|
]
|
|
})"_json;
|
|
|
|
json expected = R"({
|
|
"title": "Hello!",
|
|
"author": {
|
|
"givenName": "John"
|
|
},
|
|
"tags": [
|
|
"example"
|
|
],
|
|
"content": "This will be unchanged",
|
|
"phoneNumber": "+01-123-456-7890"
|
|
})"_json;
|
|
|
|
document.merge_patch(patch);
|
|
CHECK(document == expected);
|
|
}
|
|
|
|
SECTION("Appendix A")
|
|
{
|
|
SECTION("Example 1")
|
|
{
|
|
json original = R"({"a":"b"})"_json;
|
|
json const patch = R"({"a":"c"})"_json;
|
|
json result = R"({"a":"c"})"_json;
|
|
|
|
original.merge_patch(patch);
|
|
CHECK(original == result);
|
|
}
|
|
|
|
SECTION("Example 2")
|
|
{
|
|
json original = R"({"a":"b"})"_json;
|
|
json const patch = R"({"b":"c"})"_json;
|
|
json result = R"({"a":"b", "b":"c"})"_json;
|
|
|
|
original.merge_patch(patch);
|
|
CHECK(original == result);
|
|
}
|
|
|
|
SECTION("Example 3")
|
|
{
|
|
json original = R"({"a":"b"})"_json;
|
|
json const patch = R"({"a":null})"_json;
|
|
json result = R"({})"_json;
|
|
|
|
original.merge_patch(patch);
|
|
CHECK(original == result);
|
|
}
|
|
|
|
SECTION("Example 4")
|
|
{
|
|
json original = R"({"a":"b","b":"c"})"_json;
|
|
json const patch = R"({"a":null})"_json;
|
|
json result = R"({"b":"c"})"_json;
|
|
|
|
original.merge_patch(patch);
|
|
CHECK(original == result);
|
|
}
|
|
|
|
SECTION("Example 5")
|
|
{
|
|
json original = R"({"a":["b"]})"_json;
|
|
json const patch = R"({"a":"c"})"_json;
|
|
json result = R"({"a":"c"})"_json;
|
|
|
|
original.merge_patch(patch);
|
|
CHECK(original == result);
|
|
}
|
|
|
|
SECTION("Example 6")
|
|
{
|
|
json original = R"({"a":"c"})"_json;
|
|
json const patch = R"({"a":["b"]})"_json;
|
|
json result = R"({"a":["b"]})"_json;
|
|
|
|
original.merge_patch(patch);
|
|
CHECK(original == result);
|
|
}
|
|
|
|
SECTION("Example 7")
|
|
{
|
|
json original = R"({"a":{"b": "c"}})"_json;
|
|
json const patch = R"({"a":{"b":"d","c":null}})"_json;
|
|
json result = R"({"a": {"b": "d"}})"_json;
|
|
|
|
original.merge_patch(patch);
|
|
CHECK(original == result);
|
|
}
|
|
|
|
SECTION("Example 8")
|
|
{
|
|
json original = R"({"a":[{"b":"c"}]})"_json;
|
|
json const patch = R"({"a":[1]})"_json;
|
|
json result = R"({"a":[1]})"_json;
|
|
|
|
original.merge_patch(patch);
|
|
CHECK(original == result);
|
|
}
|
|
|
|
SECTION("Example 9")
|
|
{
|
|
json original = R"(["a","b"])"_json;
|
|
json const patch = R"(["c","d"])"_json;
|
|
json result = R"(["c","d"])"_json;
|
|
|
|
original.merge_patch(patch);
|
|
CHECK(original == result);
|
|
}
|
|
|
|
SECTION("Example 10")
|
|
{
|
|
json original = R"({"a":"b"})"_json;
|
|
json const patch = R"(["c"])"_json;
|
|
json result = R"(["c"])"_json;
|
|
|
|
original.merge_patch(patch);
|
|
CHECK(original == result);
|
|
}
|
|
|
|
SECTION("Example 11")
|
|
{
|
|
json original = R"({"a":"foo"})"_json;
|
|
json const patch = R"(null)"_json;
|
|
json result = R"(null)"_json;
|
|
|
|
original.merge_patch(patch);
|
|
CHECK(original == result);
|
|
}
|
|
|
|
SECTION("Example 12")
|
|
{
|
|
json original = R"({"a":"foo"})"_json;
|
|
json const patch = R"("bar")"_json;
|
|
json result = R"("bar")"_json;
|
|
|
|
original.merge_patch(patch);
|
|
CHECK(original == result);
|
|
}
|
|
|
|
SECTION("Example 13")
|
|
{
|
|
json original = R"({"e":null})"_json;
|
|
json const patch = R"({"a":1})"_json;
|
|
json result = R"({"e":null,"a":1})"_json;
|
|
|
|
original.merge_patch(patch);
|
|
CHECK(original == result);
|
|
}
|
|
|
|
SECTION("Example 14")
|
|
{
|
|
json original = R"([1,2])"_json;
|
|
json const patch = R"({"a":"b","c":null})"_json;
|
|
json result = R"({"a":"b"})"_json;
|
|
|
|
original.merge_patch(patch);
|
|
CHECK(original == result);
|
|
}
|
|
|
|
SECTION("Example 15")
|
|
{
|
|
json original = R"({})"_json;
|
|
json const patch = R"({"a":{"bb":{"ccc":null}}})"_json;
|
|
json result = R"({"a":{"bb":{}}})"_json;
|
|
|
|
original.merge_patch(patch);
|
|
CHECK(original == result);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("JSON Merge Patch on deeply nested values")
|
|
{
|
|
SECTION("patching past the descent bound gives the same result")
|
|
{
|
|
// every depth on either side of where the iterative version takes
|
|
// over (detail::recursion_depth_limit(), 128)
|
|
for (std::size_t depth = 0; depth <= 300; ++depth)
|
|
{
|
|
CAPTURE(depth)
|
|
for (int variant = 0; variant < 3; ++variant)
|
|
{
|
|
CAPTURE(variant)
|
|
const json patch = json::parse(nested_objects(depth, variant));
|
|
|
|
json result = json::parse(nested_objects(depth, (variant + 1) % 3));
|
|
json expected = result;
|
|
result.merge_patch(patch);
|
|
reference_merge_patch(expected, patch);
|
|
CHECK(result == expected);
|
|
|
|
// a target that is not an object, and an empty one
|
|
json from_null;
|
|
from_null.merge_patch(patch);
|
|
json expected_from_null;
|
|
reference_merge_patch(expected_from_null, patch);
|
|
CHECK(from_null == expected_from_null);
|
|
}
|
|
}
|
|
}
|
|
|
|
SECTION("patches nested too deeply for the call stack (#5393)")
|
|
{
|
|
// applying a patch used to recurse once per nesting level. The result
|
|
// is only walked, never copied or compared, since those recurse too.
|
|
const std::size_t depth = 100000;
|
|
json target = json::parse(nested_objects(depth, 0));
|
|
target.merge_patch(json::parse(nested_objects(depth, 1)));
|
|
|
|
const json* p = ⌖
|
|
for (std::size_t i = 0; i < depth; ++i)
|
|
{
|
|
p = &p->at("a");
|
|
}
|
|
// {"y":2} patched with {"x":1,"y":null}
|
|
CHECK(p->size() == 1);
|
|
CHECK(p->at("x") == 1);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("JSON Merge Patch and update on ordered_json")
|
|
{
|
|
using nlohmann::ordered_json;
|
|
|
|
SECTION("merge_patch")
|
|
{
|
|
ordered_json target = ordered_json::parse(R"({"a": {"b": 1, "c": 2}, "d": 3, "e": [1]})");
|
|
target.merge_patch(ordered_json::parse(R"({"a": {"b": null, "f": 4}, "d": {"x": {"y": null}}, "e": null, "g": {"h": 5}})"));
|
|
CHECK(target == ordered_json::parse(R"({"a": {"c": 2, "f": 4}, "d": {"x": {}}, "g": {"h": 5}})"));
|
|
|
|
// a patch that is not an object replaces the target
|
|
target.merge_patch(ordered_json({1, 2}));
|
|
CHECK(target == ordered_json({1, 2}));
|
|
// an object patch turns a target that is not an object into one
|
|
target.merge_patch(ordered_json::parse(R"({"k": {"l": null}})"));
|
|
CHECK(target == ordered_json::parse(R"({"k": {}})"));
|
|
}
|
|
|
|
SECTION("update with merge_objects")
|
|
{
|
|
ordered_json target = ordered_json::parse(R"({"a": {"b": 1, "c": {"d": 2}}, "e": 3})");
|
|
target.update(ordered_json::parse(R"({"a": {"c": {"x": 1}, "f": 4}, "e": {"y": 5}, "g": 6})"), true);
|
|
CHECK(target == ordered_json::parse(R"({"a": {"b": 1, "c": {"d": 2, "x": 1}, "f": 4}, "e": {"y": 5}, "g": 6})"));
|
|
|
|
target.update(ordered_json::parse(R"({"a": 1})"), false);
|
|
CHECK(target == ordered_json::parse(R"({"a": 1, "e": {"y": 5}, "g": 6})"));
|
|
}
|
|
}
|
|
|
|
TEST_CASE("merge_patch() with an argument that aliases *this (#5641)")
|
|
{
|
|
SECTION("j.merge_patch(j): erasing a member destroys the node the loop's iterator points to")
|
|
{
|
|
// reproduces issue #5641, case 1
|
|
json j = {{"a", nullptr}, {"b", 1}};
|
|
j.merge_patch(j);
|
|
CHECK(j == json({{"b", 1}}));
|
|
}
|
|
|
|
SECTION("j.merge_patch(j[\"a\"]): removing \"a\" destroys the patch while it is iterated")
|
|
{
|
|
// reproduces issue #5641, case 2
|
|
json j = {{"a", {{"a", nullptr}, {"b", 2}}}};
|
|
j.merge_patch(j["a"]);
|
|
CHECK(j == json({{"b", 2}}));
|
|
}
|
|
|
|
SECTION("a patch nested past the iterative descent bound aliases *this")
|
|
{
|
|
// every depth on either side of where the iterative version takes
|
|
// over (detail::recursion_depth_limit(), 128); patching *this with
|
|
// itself is idempotent, aliased or not
|
|
for (const std::size_t depth :
|
|
{
|
|
std::size_t{0}, std::size_t{127}, std::size_t{128}, std::size_t{300}
|
|
})
|
|
{
|
|
CAPTURE(depth)
|
|
json j = json::parse(nested_objects(depth, 0));
|
|
const json expected = j;
|
|
j.merge_patch(j);
|
|
CHECK(j == expected);
|
|
}
|
|
}
|
|
|
|
SECTION("ordered_json")
|
|
{
|
|
using nlohmann::ordered_json;
|
|
|
|
SECTION("merge_patch with a member of *this")
|
|
{
|
|
ordered_json j = {{"a", {{"a", nullptr}, {"b", 2}}}};
|
|
j.merge_patch(j["a"]);
|
|
CHECK(j == ordered_json({{"b", 2}}));
|
|
}
|
|
}
|
|
}
|
|
|
|
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());
|
|
}
|
|
}
|