// __ _____ _____ _____ // __| | __| | | | 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; using nlohmann::json_document; using nlohmann::json_editable_document; using nlohmann::json_editable_view; using nlohmann::ordered_json_document; using nlohmann::ordered_json_editable_document; using nlohmann::ordered_json_editable_view; using ptr_t = ordered_json::json_pointer; #include #include #include #include #include #include #include #include #include #include #include #include namespace { std::uint32_t rng() { static std::mt19937 generator(5295); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible // result_type is std::uint_fast32_t, which may be wider than 32 bits const std::mt19937::result_type value = generator(); return static_cast(value); } int r(int n) { return static_cast(rng() % static_cast(n)); } int counter = 0; std::uint64_t bits(double x) { std::uint64_t b = 0; std::memcpy(&b, &x, sizeof(b)); return b; } std::string random_string() { static const std::array pieces = {{"a", "Z", " ", "~", "\n", "\"", "\\", "/", "\xc3\xa9", "\xe3\x81\x82", "\xf0\x9f\x98\x80", "\x7f", "\x1f", "0", "key"}}; std::string s; for (int i = r(3) == 0 ? r(30) : r(6); i > 0; --i) { s += pieces[static_cast(r(15))]; } return s; } ordered_json random_scalar() { switch (r(9)) { case 0: return nullptr; case 1: return r(2) == 0; case 2: return static_cast(rng()) - 2147483648LL; case 3: return (static_cast(rng()) * 4294967296ULL) + rng(); case 4: return static_cast(static_cast(rng())) / (1 + r(1000)); case 5: return r(2) == 0 ? 1e300 * (r(2) == 0 ? 1 : -1) : 5e-324; case 6: return -0.0; default: return random_string(); } } ordered_json random_value(int depth) { const int k = depth > 3 ? 4 : r(7); if (k == 0) { ordered_json o = ordered_json::object(); for (int i = r(5); i > 0; --i) { o[random_string() + "#" + std::to_string(counter++)] = random_value(depth + 1); } return o; } if (k == 1) { ordered_json a = ordered_json::array(); for (int i = r(5); i > 0; --i) { a.push_back(random_value(depth + 1)); } return a; } return random_scalar(); } void collect(const ordered_json& j, const ptr_t& p, std::vector& out) { out.push_back(p); if (j.is_object()) { for (const auto& kv : j.items()) { collect(kv.value(), p / kv.key(), out); } } else if (j.is_array()) { for (std::size_t i = 0; i < j.size(); ++i) { collect(j[i], p / i, out); } } } // the edited view and the ordered_json value read the same through the whole // read API void compare(const ordered_json_editable_view& v, const ordered_json& j) { REQUIRE(v.type() == j.type()); REQUIRE(v.size() == j.size()); if (j.is_object()) { auto jt = j.begin(); for (auto it = v.begin(); it != v.end(); ++it, ++jt) { CHECK(std::string(it.key().data(), it.key().size()) == jt.key()); CHECK(v[jt.key()].dump() == jt.value().dump()); CHECK(v.contains(jt.key())); compare(*it, jt.value()); } } else if (j.is_array()) { for (std::size_t i = 0; i < j.size(); ++i) { CHECK(v[i].dump() == j[i].dump()); } std::size_t i = 0; for (const auto e : v) { compare(e, j[i++]); } if (!j.empty()) { CHECK(v.back().dump() == j.back().dump()); } } else if (j.is_string()) { CHECK(v.get() == j.get()); } else if (j.is_number_float()) { CHECK(bits(v.get()) == bits(j.get())); } else if (j.is_number_integer()) { CHECK(v.get() == j.get()); CHECK(v.get() == j.get()); } } // the text of j, in which some objects repeat a key of theirs after their // members: lookups of the edited document find the first member of a key, so // j (the first members) is what they must agree with, while parse() would // keep the last value std::string text_with_duplicates(const ordered_json& j) { if (j.is_object()) { std::vector keys; for (const auto& kv : j.items()) { keys.push_back(kv.key()); } const auto repeated = [&keys]() { return ordered_json(keys[static_cast(r(static_cast(keys.size())))]).dump() + ":" + random_value(2).dump(); }; std::string text = "{"; bool first = true; for (const auto& kv : j.items()) { text += (first ? "" : ",") + ordered_json(kv.key()).dump() + ":" + text_with_duplicates(kv.value()); first = false; } for (int i = keys.empty() ? 0 : r(3); i > 0; --i) { text += "," + repeated(); } return text + "}"; } if (j.is_array()) { std::string text = "["; for (std::size_t i = 0; i < j.size(); ++i) { text += (i != 0 ? "," : "") + text_with_duplicates(j[i]); } return text + "]"; } return j.dump(); } // every lookup of the edited view finds the first member of its key, which // the iteration of the view shows void check_lookups(const ordered_json_editable_view& v) { if (v.is_object()) { std::set seen; for (const auto item : v.items()) { const std::string key(item.key().data(), item.key().size()); if (!seen.insert(key).second) { continue; // (not the first member of the key) } const ptr_t ptr = ptr_t() / key; CAPTURE(key) const ordered_json expected = item.value().materialize(); const ordered_json_editable_view m = v[key]; REQUIRE(!m.is_discarded()); CHECK(m.materialize() == expected); CHECK(v.at(key).materialize() == expected); CHECK(v[ptr].materialize() == expected); CHECK(v.at(ptr).materialize() == expected); CHECK(v.contains(key)); CHECK(v.contains(ptr)); CHECK(v.count(key) == 1); const auto it = v.find(key); REQUIRE(it != v.end()); CHECK((*it).materialize() == expected); CHECK(v.value(ptr, ordered_json(nullptr)) == expected); if (expected.is_string()) { CHECK(v.value(key, std::string("-")) == expected.get()); } check_lookups(m); } CHECK(v["missing#key"].is_discarded()); CHECK(!v.contains("missing#key")); CHECK(v.find("missing#key") == v.end()); } else if (v.is_array()) { for (std::size_t i = 0; i < v.size(); ++i) // NOLINT(modernize-loop-convert): both v[i] and v.at(i) are tested { check_lookups(v[i]); check_lookups(v.at(i)); } } } // every lookup of the edited view finds what the first members of j hold void check_first_members(const ordered_json_editable_view& v, const ordered_json& j) { REQUIRE(v.type() == j.type()); if (j.is_object()) { for (const auto& kv : j.items()) { const std::string& key = kv.key(); const ptr_t ptr = ptr_t() / key; CAPTURE(key) REQUIRE(v.contains(key)); CHECK(v.contains(ptr)); CHECK(v.count(key) == 1); CHECK(v.find(key) != v.end()); CHECK(v.at(ptr).type() == kv.value().type()); check_first_members(v[key], kv.value()); check_first_members(v.at(key), kv.value()); check_first_members(v[ptr], kv.value()); } CHECK(!v.contains("missing#key")); } else if (j.is_array()) { REQUIRE(v.size() == j.size()); for (std::size_t i = 0; i < j.size(); ++i) { check_first_members(v[i], j[i]); } } else { CHECK(v.materialize() == j); } } // (for documents whose objects repeat keys: j holds the first members, the // lookups must find them; dump(), size(), and iteration list all members, but // materialize() keeps the last value of a key, as parse() does of the dump) void check_duplicates(const ordered_json_editable_document& d, const ordered_json& j) { check_first_members(d.root(), j); check_lookups(d.root()); CHECK(d.root().materialize() == ordered_json::parse(d.root().dump())); } void check_all(const ordered_json_editable_document& d, const ordered_json& j, bool deep) { const std::string text = d.root().dump(); REQUIRE(text == j.dump()); CHECK(d.root().materialize() == j); if (deep) { CHECK(d.root().dump(2) == j.dump(2)); CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true)); CHECK(d.root() == j); compare(d.root(), j); // a fresh document of the text reads the same const ordered_json_document fresh = ordered_json_document::parse(text); CHECK(fresh.root() == d.root()); } } } // namespace TEST_CASE("json_view edits: differential") { // random edits are applied to an ordered_json_editable_document and to the // ordered_json parse() produces; after every edit both must serialize, // materialize, and read back the same for (int n = 0; n < 250; ++n) { // the last 100 documents repeat keys in some of their objects; j then // holds the first member of each key, which the edits address const bool duplicates = n >= 150; ordered_json j = random_value(0); if (r(4) == 0) { j = ordered_json::object({{"a", random_value(1)}, {"b", random_value(1)}}); } if (duplicates) { j = ordered_json::parse(j.dump()); // (normalized as the document reads it) } const std::string text = duplicates ? text_with_duplicates(j) : j.dump(r(2) == 0 ? -1 : 2); CAPTURE(text) ordered_json_editable_document d = ordered_json_editable_document::parse(text); if (!duplicates) { j = ordered_json::parse(text); } const ordered_json_document other = ordered_json_document::parse(random_value(0).dump()); const int edits = 30; for (int e = 0; e < edits; ++e) { std::vector paths; collect(j, ptr_t(), paths); const ptr_t p = paths[static_cast(r(static_cast(paths.size())))]; const ordered_json& target = j[p]; const ordered_json_editable_view tv = d.root().at(p); const int op = r(12); { if (op == 0) // assign a scalar { const ordered_json v = random_scalar(); if (v.is_string() && r(2) == 0) { d.set(tv, v.get()); } else if (v.is_number_unsigned() && r(2) == 0) { d.set(tv, v.get()); } else { d.set(tv, v); } j[p] = v; } else if (op == 1) // assign a new array/object (or anything), sometimes via the pointer API { const ordered_json v = random_value(2); if (r(2) == 0) { d.set(p, v); } else { d.set(tv, v); } j[p] = v; } else if (op == 2) // copy a value of the same document { const ptr_t& q = paths[static_cast(r(static_cast(paths.size())))]; const ordered_json v = j[q]; d.set(tv, d.root().at(q)); j[p] = v; } else if (op == 3) // copy a value of another document { d.set(tv, other.root()); j[p] = other.root().materialize(); } else if ((op == 4 || op == 5) && target.is_object()) // set a member (new or existing) { std::string key = random_string() + "#" + std::to_string(counter++); if (op == 5 && !target.empty()) { key = std::next(target.begin(), r(static_cast(target.size()))).key(); } const ordered_json v = random_value(2); d.set(tv, key, v); j[p][key] = v; } else if (op == 6 && target.is_object() && !target.empty()) // erase a member { const std::string key = std::next(target.begin(), r(static_cast(target.size()))).key(); if (r(2) == 0) { d.erase(tv, key); } else { d.erase(p / key); } j[p].erase(key); } else if (op == 7 && (target.is_array() || target.is_null())) // push_back { const ordered_json v = random_value(2); d.push_back(tv, v); j[p].push_back(v); } else if (op == 8 && target.is_array()) // insert { const auto i = static_cast(r(static_cast(target.size()) + 1)); const ordered_json v = random_value(2); d.insert(tv, i, v); j[p].insert(j[p].begin() + static_cast(i), v); } else if (op == 9 && target.is_array() && !target.empty()) // erase an element { const auto i = static_cast(r(static_cast(target.size()))); if (r(2) == 0) { d.erase(tv, i); } else { d.erase(p / i); } j[p].erase(i); } else if (op == 10 && target.is_array() && !target.empty()) // assign an element { const auto i = static_cast(r(static_cast(target.size()))); const ordered_json v = random_value(2); d.set(tv, i, v); j[p][i] = v; } else if (op == 11) // a held view sees the assignment { const ordered_json v = random_value(2); const ordered_json_editable_view held = d.root().at(p); d.set(p, v); j[p] = v; CHECK(held.dump() == v.dump()); } else { continue; } } CAPTURE(p.to_string()) CAPTURE(op) if (duplicates) { check_duplicates(d, j); } else { check_all(d, j, e % 8 == 7 || e == edits - 1); } } } } namespace { #if !defined(JSON_NOEXCEPTION) // the exception a call throws, or "" if it throws none std::string exception_of_call(const std::function& f) { try { f(); } catch (const json::exception& e) { return e.what(); } return ""; } #endif } // namespace TEST_CASE("json_view edits: errors") { SECTION("an empty document") { json_editable_document d; CHECK_THROWS_WITH_AS(d.set(d.root(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&); } json_editable_document d = json_editable_document::parse(R"({"o": {"a": 1}, "a": [1, 2], "n": 1, "z": null})"); const json_editable_view root = d.root(); const json_document other = json_document::parse("[1]"); json_editable_document other_editable = json_editable_document::parse("[1]"); CHECK_THROWS_WITH_AS(d.set(other_editable.root(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&); CHECK_THROWS_WITH_AS(d.set(json_editable_view(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&); CHECK_THROWS_WITH_AS(d.set(root["a"], "k", 1), "[json.exception.type_error.305] cannot use operator[] with a string argument with array", json::type_error&); CHECK_THROWS_WITH_AS(d.set(root["o"], 0, 1), "[json.exception.type_error.305] cannot use operator[] with a numeric argument with object", json::type_error&); CHECK_THROWS_WITH_AS(d.set(root["a"], 2, 1), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&); CHECK_THROWS_WITH_AS(d.set(root["a"], -1, 1), "[json.exception.out_of_range.401] array index -1 is out of range", json::out_of_range&); CHECK_THROWS_WITH_AS(d.push_back(root["o"], 1), "[json.exception.type_error.308] cannot use push_back() with object", json::type_error&); CHECK_THROWS_WITH_AS(d.insert(root["n"], 0, 1), "[json.exception.type_error.309] cannot use insert() with number", json::type_error&); CHECK_THROWS_WITH_AS(d.insert(root["a"], 3, 1), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&); CHECK_THROWS_WITH_AS(d.erase(root["n"], "k"), "[json.exception.type_error.307] cannot use erase() with number", json::type_error&); CHECK_THROWS_WITH_AS(d.erase(root["o"], 0), "[json.exception.type_error.307] cannot use erase() with object", json::type_error&); CHECK_THROWS_WITH_AS(d.erase(root["a"], 2), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&); CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("")), "[json.exception.out_of_range.405] JSON pointer has no parent", json::out_of_range&); CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("/missing/x")), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&); CHECK_THROWS_WITH_AS(d.set(json::json_pointer("/a/01"), 1), "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&); CHECK_THROWS_WITH_AS(d.set(root, json_editable_view()), "[json.exception.type_error.302] type must be a value, but is discarded", json::type_error&); CHECK_THROWS_WITH_AS(d.set(root, json::binary({1, 2})), "[json.exception.type_error.319] cannot store a binary value in a json_document", json::type_error&); #if !defined(JSON_NOEXCEPTION) // invalid UTF-8 is rejected when it enters the document, with the error // basic_json::dump() reports for the same string for (const std::string bad : {"\xC3\x28", "a\xE2\x28\xA1", "\xF0\x28\x8C\xBC", "\xE2\x82", "x\xF0\x9F\x98", "\xFF", "\xED\xA0\x80", "\xC0\xAF" }) { CAPTURE(bad) const std::string expected = exception_of_call([&] { const std::string text = json(bad).dump(); static_cast(text); }); CHECK(!expected.empty()); CHECK(exception_of_call([&] { d.set(root["z"], bad); }) == expected); CHECK(exception_of_call([&] { d.set(root["o"], bad, 1); }) == expected); CHECK(exception_of_call([&] { d.set(root["z"], json{{"k", bad}}); }) == expected); } #endif // nothing of the failed edits is visible CHECK(root.dump() == R"({"o":{"a":1},"a":[1,2],"n":1,"z":null})"); static_cast(other); } TEST_CASE("json_view edits: views and values") { SECTION("a value that is no longer part of the document") { json_editable_document d = json_editable_document::parse("[[[1,2]]]"); const json_editable_view inner = d.root()[0][0]; d.set(d.root()[0], json::array({7})); d.set(inner, 5); CHECK(d.root().dump() == "[[7]]"); CHECK(inner.get() == 5); } SECTION("views keep referring to their value") { json_editable_document d = json_editable_document::parse(R"({"a": [10, 20, 30], "b": {"c": "text"}})"); const json_editable_view a = d.root()["a"]; const json_editable_view twenty = a[1]; const json_editable_view c = d.root()["b"]["c"]; d.insert(a, 0, 5); d.push_back(a, 40); CHECK(twenty.get() == 20); CHECK(a[2].get() == 20); d.erase(a, 2); CHECK(twenty.get() == 20); // an erased value keeps its last value d.set(c, 7); CHECK(c.get() == 7); // a held view sees an assignment d.set(d.root()["b"], json::array({1, 2})); CHECK(d.root()["b"].dump() == "[1,2]"); CHECK(d.root().dump() == R"({"a":[5,10,30,40],"b":[1,2]})"); CHECK(d.root()["a"][0].source_offset() == static_cast(-1)); // a new value CHECK(d.root()["a"][1].source_offset() != static_cast(-1)); } SECTION("strings stay valid while more edits come") { json_editable_document d = json_editable_document::parse("[]"); const auto first = d.push_back(d.root(), std::string(100, 'x')).get_string(); for (int i = 0; i < 1000; ++i) { d.push_back(d.root(), std::string(static_cast(i % 50), 'y')); } CHECK(std::string(first.data(), first.size()) == std::string(100, 'x')); CHECK(d.root().size() == 1001); } SECTION("numbers") { json_editable_document d = json_editable_document::parse(R"([1.50, 1E2, 3])"); d.set(d.root(), 2, 0.1); d.push_back(d.root(), std::numeric_limits::quiet_NaN()); d.push_back(d.root(), -std::numeric_limits::infinity()); d.push_back(d.root(), (std::numeric_limits::max)()); d.push_back(d.root(), (std::numeric_limits::min)()); CHECK(d.root().dump() == "[1.5,100.0,0.1,null,null,18446744073709551615,-9223372036854775808]"); CHECK(d.root().dump(-1, ' ', false, json_editable_view::number_format::source) == "[1.50,1E2,0.1,null,null,18446744073709551615,-9223372036854775808]"); CHECK(std::isnan(d.root()[3].get())); CHECK(std::isinf(d.root()[4].get())); CHECK(d.root()[2].number_token() == "0.1"); CHECK(d.root()[5].get() == 18446744073709551615u); CHECK(d.root()[6].number_token() == "-9223372036854775808"); CHECK(d.root().materialize().dump() == json::parse(R"([1.5, 100.0, 0.1, null, null, 18446744073709551615, -9223372036854775808])").dump()); } SECTION("numbers of other float types") { // doubles have their own path to the output; other float types are // written as basic_json writes them, non-finite values as null using json_float = nlohmann::basic_json; using document_float = nlohmann::basic_json_document; document_float d = document_float::parse("[1.5]"); d.push_back(d.root(), std::numeric_limits::quiet_NaN()); d.push_back(d.root(), -std::numeric_limits::infinity()); CHECK(d.root().dump() == "[1.5,null,null]"); CHECK(d.root().dump(2) == json_float::parse("[1.5, null, null]").dump(2)); } SECTION("nulls become containers, and the root can be replaced") { json_editable_document d = json_editable_document::parse("[null, null]"); d.set(d.root()[0], "k", 1); d.push_back(d.root()[1], true); CHECK(d.root().dump() == R"([{"k":1},[true]])"); d.set(d.root(), "scalar"); CHECK(d.root().dump() == R"("scalar")"); d.set(d.root(), json{{"x", {1, 2}}}); d.set(json::json_pointer("/x/-"), 3); d.set(json::json_pointer("/x/3"), 4); // the size of the array appends too d.set(json::json_pointer("/y"), false); CHECK(d.root().dump() == R"({"x":[1,2,3,4],"y":false})"); CHECK(d.erase(json::json_pointer("/x/0")) == 1); CHECK(d.erase(json::json_pointer("/y")) == 1); CHECK(d.erase(json::json_pointer("/nothing")) == 0); CHECK(d.root().dump() == R"({"x":[2,3,4]})"); } SECTION("duplicate keys") { json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})"); d.set(d.root(), "a", 4); // the first member is assigned, the others dropped CHECK(d.root().dump() == R"({"a":4,"b":2})"); d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})"); CHECK(d.erase(d.root(), "a") == 2); CHECK(d.root().dump() == R"({"b":2})"); } SECTION("values from other documents") { const json_document source = json_document::parse(R"({"list": [1, "two", {"three": 3.5}], "text": "a\nb"})"); json_editable_document edited = json_editable_document::parse("[0]"); edited.set(edited.root(), 0, json{{"inner", {1, 2}}}); json_editable_document d = json_editable_document::parse("{}"); d.set(d.root(), "copy", source.root()["list"]); d.set(d.root(), "text", source.root()["text"]); d.set(d.root(), "edited", edited.root()[0]); d.set(d.root(), "self", d.root()["copy"]); CHECK(d.root().dump() == R"({"copy":[1,"two",{"three":3.5}],"text":"a\nb","edited":{"inner":[1,2]},"self":[1,"two",{"three":3.5}]})"); CHECK(d.root()["copy"] == source.root()["list"]); CHECK(source.root()["list"] == d.root()["self"]); CHECK(d.root() != source.root()); } SECTION("large objects") { std::string text = "{"; for (int i = 0; i < 200; ++i) { text += (i != 0 ? ",\"k" : "\"k") + std::to_string(i) + "\":" + std::to_string(i); } text += '}'; json_editable_document d = json_editable_document::parse(text); d.set(d.root(), "k7", "seven"); // assigned in place: the index stays in use CHECK(d.root()["k7"].get_string() == "seven"); d.set(d.root(), "new", 1); // appended: the members move, the lookup is linear CHECK(d.root()["new"].get() == 1); CHECK(d.root()["k199"].get() == 199); d.erase(d.root(), "k0"); CHECK(!d.root().contains("k0")); CHECK(d.root().size() == 200); } SECTION("reuse and memory") { json_editable_document d = json_editable_document::parse("[1, 2, 3]"); const std::size_t before = d.memory_usage(); for (int i = 0; i < 100; ++i) { d.push_back(d.root(), "some text"); } CHECK(d.memory_usage() > before); const json_editable_view first = d.root()[0]; d.shrink_to_fit(); // (with edits, the index stays in place) CHECK(first.get() == 1); d.read(std::string("[true]")); CHECK(d.root().dump() == "[true]"); d.push_back(d.root(), false); CHECK(d.root().dump() == "[true,false]"); } } TEST_CASE("json_view edits: deeply nested values") { // copying a value into a document must not recurse per nesting level const std::size_t depth = 100000; const std::string brackets = std::string(depth, '[') + std::string(depth, ']'); std::string braces; for (std::size_t i = 0; i < depth; ++i) { braces += "{\"a\":"; } braces += '1'; braces += std::string(depth, '}'); SECTION("a view of a read-only document") { const json_document source = json_document::parse(brackets); json_editable_document d = json_editable_document::parse("[]"); d.push_back(d.root(), source.root()); CHECK(d.root().dump() == "[" + brackets + "]"); } SECTION("a view of an editable document") { const json_editable_document source = json_editable_document::parse(braces); json_editable_document d = json_editable_document::parse("{}"); d.set(d.root(), "deep", source.root()); CHECK(d.root().dump() == "{\"deep\":" + braces + "}"); } SECTION("a view of an edited document (values behind links)") { const json_document source = json_document::parse(brackets); json_editable_document edited = json_editable_document::parse("[[]]"); edited.push_back(edited.root()[0], source.root()); edited.push_back(edited.root(), source.root()); json_editable_document d = json_editable_document::parse("null"); d.set(d.root(), edited.root()); CHECK(d.root().dump() == "[[" + brackets + "]," + brackets + "]"); } SECTION("a basic_json value") { json deep = json::array(); json* inner = &deep; for (std::size_t i = 1; i < depth; ++i) { inner->push_back(json::array()); inner = &inner->back(); } json_editable_document d = json_editable_document::parse("[]"); d.push_back(d.root(), deep); CHECK(d.root().dump() == "[" + brackets + "]"); } SECTION("a basic_json value with objects") { json deep = 1; for (std::size_t i = 0; i < depth; ++i) { json outer = json::object(); outer["a"] = std::move(deep); deep = std::move(outer); } json_editable_document d = json_editable_document::parse("{}"); d.set(d.root(), "deep", deep); CHECK(d.root().dump() == "{\"deep\":" + braces + "}"); } } #if !defined(JSON_NOEXCEPTION) TEST_CASE("json_view edits: pointers below a null value") { // a null value on the way becomes what basic_json makes of it: an array // for "-" and for digits, an object otherwise struct test_case { const char* document; const char* pointer; }; const std::array cases = { { {R"({"a":null})", "/a/0"}, {R"({"a":null})", "/a/-"}, {R"({"a":null})", "/a/3"}, {R"({"a":null})", "/a/x"}, {R"({"a":null})", "/a/+1"}, {R"({"a":null})", "/a/01"}, {R"({"a":null})", "/a/"}, {R"({"a":{"b":null}})", "/a/b/1"}, {R"({"a":{"b":null}})", "/a/b/-"}, {R"({"a":[null]})", "/a/0/0"}, {R"({"a":[null,null]})", "/a/1/k"}, {R"([null])", "/0"}, {"null", "/0"}, {"null", "/-"}, {"null", "/k"}, {"null", ""}, } }; for (const test_case& c : cases) { CAPTURE(c.document) CAPTURE(c.pointer) json expected = json::parse(c.document); const std::string error = exception_of_call([&] { expected[json::json_pointer(c.pointer)] = 1; }); json_editable_document d = json_editable_document::parse(c.document); if (error.empty()) { d.set(json::json_pointer(c.pointer), 1); CHECK(d.root().dump() == expected.dump()); CHECK(d.root().materialize() == expected); } else { // the same error, and the document is not changed CHECK(exception_of_call([&] { d.set(json::json_pointer(c.pointer), 1); }) == error); CHECK(d.root().dump() == json::parse(c.document).dump()); } } } TEST_CASE("json_view edits: strings of other documents are checked") { // A document borrows the text it was parsed from, and sees later changes // of the text: a way to get ill-formed UTF-8 into a view. Copying it into // an editable document is an error, as for any other string. std::string text = R"({"key":"abc","list":["abc"]})"; const json_document source = json_document::parse(text); const auto message_of = [](const std::string & bad) { return exception_of_call([&] { const std::string dumped = json(bad).dump(); static_cast(dumped); }); }; json_editable_document d = json_editable_document::parse("[1]"); d.push_back(d.root(), source.root()); CHECK(d.root().dump() == R"([1,{"key":"abc","list":["abc"]}])"); text[text.find("abc") + 1] = '\xC3'; // "a\xC3c" text[text.rfind("abc") + 1] = '\xC3'; const std::string bad_value = message_of(std::string("a\xC3" "c")); CHECK(!bad_value.empty()); CHECK(exception_of_call([&] { d.push_back(d.root(), source.root()["key"]); }) == bad_value); CHECK(exception_of_call([&] { d.set(d.root()[0], source.root()["list"][0]); }) == bad_value); CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()["list"]); }) == bad_value); CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()); }) == bad_value); text[text.find("key") + 1] = '\xC3'; // a key is checked as well const std::string bad_key = message_of(std::string("k\xC3" "y")); CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()); }) == bad_key); CHECK(exception_of_call([&] { d.push_back(d.root(), source.root()); }) == bad_key); // nothing of the failed edits is visible CHECK(d.root().dump() == R"([1,{"key":"abc","list":["abc"]}])"); } TEST_CASE("json_view edits: the size of a text arena") { using nlohmann::detail::view::text_capacity; constexpr std::size_t limit = 0xFFFFFFFFu; // grows by doubling, or to what is needed (plus some room) CHECK(text_capacity(0, 0, 10) == 266); CHECK(text_capacity(1000, 990, 20) == 2000); CHECK(text_capacity(100, 100, 5000) == 5356); // an arena beyond 2 GiB: doubling is clamped to 4 GiB - 1 CHECK(text_capacity(0x90000000u, 0x8FFFFFFFu, 2) == limit); CHECK(text_capacity(limit, limit - 10, 10) == limit); // exactly what fits is accepted, without room to spare CHECK(text_capacity(100, 90, limit - 90) == limit); CHECK(text_capacity(limit - 100, limit - 100, 100) == limit); // what does not fit is an error CHECK_THROWS_WITH_AS(text_capacity(100, 90, limit - 89), "[json.exception.out_of_range.416] edits of 4 GiB or more are not supported by json_document", json::out_of_range&); CHECK_THROWS_WITH_AS(text_capacity(limit, limit, 1), "[json.exception.out_of_range.416] edits of 4 GiB or more are not supported by json_document", json::out_of_range&); } #endif TEST_CASE("json_view edits: replacing arrays and objects by scalars") { // the first assignment switches the parent to links; the later ones do // not need to look for the parent again std::string text = "["; json expected = json::array(); for (int i = 0; i < 300; ++i) { text += (i != 0 ? ",[" : "[") + std::to_string(i) + ",{\"k\":" + std::to_string(i) + "}]"; expected.push_back(json::array({i, json{{"k", i}}})); } text += ']'; json_editable_document d = json_editable_document::parse(text); CHECK(d.root().dump() == expected.dump()); for (int i = 0; i < 300; i += 2) { d.set(d.root()[static_cast(i)], i); expected[static_cast(i)] = i; } CHECK(d.root().dump() == expected.dump()); for (int i = 1; i < 300; i += 2) // (elements that are still in the parsed layout of their parent) { d.set(d.root()[static_cast(i)][1], "x"); // replaces an object expected[static_cast(i)][1] = "x"; } CHECK(d.root().dump() == expected.dump()); // new values, and values in new values d.push_back(d.root(), json::parse(R"([[1,2],{"a":[3]}])")); expected.push_back(json::parse(R"([[1,2],{"a":[3]}])")); d.set(d.root()[300][0], 7); expected[300][0] = 7; d.insert(d.root(), 0, json::array({1, 2})); expected.insert(expected.begin(), json::array({1, 2})); d.set(d.root()[0], nullptr); expected[0] = nullptr; d.set(d.root()[301], 5); expected[301] = 5; CHECK(d.root().dump() == expected.dump()); CHECK(d.root().materialize() == expected); } namespace { // reads of "a" in an object that repeats it: the first member wins, however the // object is stored template void check_first_wins(const View& o, int expected) { CAPTURE(expected) CHECK(o["a"].template get() == expected); CHECK(o.at("a").template get() == expected); CHECK(o.find("a")->template get() == expected); CHECK(o.value("a", -1) == expected); CHECK(o[json::json_pointer("/a")].template get() == expected); CHECK(o.at(json::json_pointer("/a")).template get() == expected); CHECK(o.value(json::json_pointer("/a"), -1) == expected); CHECK(o.contains("a")); CHECK(o.contains(json::json_pointer("/a"))); CHECK(o.count("a") == 1); } } // namespace TEST_CASE("json_view edits: duplicate keys") { SECTION("an object in its parsed layout, then moved by edits") { json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})"); check_first_wins(d.root(), 1); CHECK(d.root().size() == 3); CHECK(d.root().dump() == R"({"a":1,"b":2,"a":3})"); CHECK(d.root().materialize()["a"] == 3); // (parse() keeps the last value) // an edit of a value does not move the object d.set(d.root()["b"], 5); check_first_wins(d.root(), 1); d.set(d.root()["a"], 4); // the member that lookups find check_first_wins(d.root(), 4); CHECK(d.root().dump() == R"({"a":4,"b":5,"a":3})"); // an appended member moves it d.set(d.root(), "c", true); check_first_wins(d.root(), 4); CHECK(d.root().size() == 4); CHECK(d.root().dump() == R"({"a":4,"b":5,"a":3,"c":true})"); d.set(d.root()["a"], 6); check_first_wins(d.root(), 6); CHECK(d.root().dump() == R"({"a":6,"b":5,"a":3,"c":true})"); d.set(json::json_pointer("/a"), 7); check_first_wins(d.root(), 7); } SECTION("set assigns the member that lookups find") { // the key stays where its first occurrence is, the later members are dropped ordered_json_editable_document d = ordered_json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3, "c": 4, "a": 5})"); const ordered_json_editable_view held = d.root()["a"]; CHECK(held.get() == 1); const ordered_json_editable_view assigned = d.set(d.root(), "a", "x"); CHECK(held.get() == "x"); CHECK(assigned.get() == "x"); CHECK(d.root().dump() == R"({"a":"x","b":2,"c":4})"); CHECK(d.root().materialize() == ordered_json::parse(R"({"a": "x", "b": 2, "c": 4})")); CHECK(d.root().size() == 3); // as for the object that parse() makes of the text ordered_json j = ordered_json::parse(R"({"a": 1, "b": 2, "a": 3, "c": 4, "a": 5})"); j["a"] = "x"; CHECK(d.root().materialize().dump() == j.dump()); // through a pointer, below a duplicate d = ordered_json_editable_document::parse(R"({"a": {"x": 1}, "b": 2, "a": {"x": 3}})"); d.set(ptr_t("/a/x"), 4); CHECK(d.root().dump() == R"({"a":{"x":4},"b":2,"a":{"x":3}})"); d.set(ptr_t("/a"), 0); CHECK(d.root().dump() == R"({"a":0,"b":2})"); } SECTION("erase removes every member") { json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})"); d.set(d.root(), "c", 4); check_first_wins(d.root(), 1); CHECK(d.erase(d.root(), "a") == 2); CHECK(d.root().dump() == R"({"b":2,"c":4})"); CHECK(!d.root().contains("a")); CHECK(d.root()["a"].is_discarded()); CHECK(d.erase(d.root(), "a") == 0); d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})"); CHECK(d.erase(json::json_pointer("/a")) == 2); CHECK(d.root().dump() == R"({"b":2})"); } SECTION("a large object with an index") { std::string text = R"({"a":0,"k7":"first")"; for (int i = 0; i < 200; ++i) { text += ",\"k" + std::to_string(i) + "\":" + std::to_string(i); } text += R"(,"a":1,"k7":"last","a":2})"; json_editable_document d = json_editable_document::parse(text); check_first_wins(d.root(), 0); CHECK(d.root()["k7"].get_string() == "first"); CHECK(d.root()["k199"].get() == 199); // a value assigned in place: the index stays in use d.set(d.root()["a"], 3); check_first_wins(d.root(), 3); d.set(d.root()["k7"], "changed"); CHECK(d.root()["k7"].get_string() == "changed"); CHECK(d.root()["k199"].get() == 199); // an appended member moves the members: the lookup scans them d.set(d.root(), "new", 1); check_first_wins(d.root(), 3); CHECK(d.root()["k7"].get_string() == "changed"); CHECK(d.root()["k7"].get_string() == d.root().at("k7").get_string()); CHECK(d.root()["k199"].get() == 199); CHECK(d.root()["new"].get() == 1); // a value replaced by a container, in an object that was not moved d = json_editable_document::parse(text); d.set(d.root()["a"], json{{"x", 1}}); CHECK(d.root()["a"]["x"].get() == 1); CHECK(d.root()["k7"].get_string() == "first"); CHECK(d.erase(d.root(), "k7") == 3); // (the key occurs three times) CHECK(d.root()["k7"].is_discarded()); CHECK(d.root()["a"]["x"].get() == 1); } SECTION("objects in moved arrays and in new values") { json_editable_document d = json_editable_document::parse(R"([{"a": 1, "a": 2}, {"a": 3, "b": 0, "a": 4}])"); check_first_wins(d.root()[0], 1); check_first_wins(d.root()[1], 3); d.insert(d.root(), 0, json::parse(R"({"a": 0})")); d.push_back(d.root(), json::parse(R"({"a": 5, "a": 6})")); // (a basic_json value has one member) check_first_wins(d.root()[1], 1); check_first_wins(d.root()[2], 3); CHECK(d.root()[3]["a"].get() == 6); d.set(d.root()[1], "a", 8); check_first_wins(d.root()[1], 8); CHECK(d.root()[1].dump() == R"({"a":8})"); d.erase(d.root(), 0); check_first_wins(d.root()[1], 3); CHECK(d.root().dump() == R"([{"a":8},{"a":3,"b":0,"a":4},{"a":6}])"); } SECTION("values of other documents") { const json_document source = json_document::parse(R"({"list": [{"a": 1, "a": 2}], "o": {"b": {"a": 3, "a": 4}, "a": 5, "a": 6}})"); json_editable_document d = json_editable_document::parse("{}"); d.set(d.root(), "copy", source.root()["list"]); d.set(d.root(), "o", source.root()["o"]); // (the copies keep the repeated members) CHECK(d.root().dump() == R"({"copy":[{"a":1,"a":2}],"o":{"b":{"a":3,"a":4},"a":5,"a":6}})"); check_first_wins(d.root()["copy"][0], 1); check_first_wins(d.root()["o"]["b"], 3); check_first_wins(d.root()["o"], 5); d.set(d.root()["o"]["b"], "c", 0); check_first_wins(d.root()["o"]["b"], 3); d.set(d.root()["o"], "d", 0); check_first_wins(d.root()["o"], 5); CHECK(d.root()["o"]["b"]["a"].get() == 3); CHECK(d.root()["o"]["d"].get() == 0); } }