From 772ccbea342fd41e54252eec11f688382849737e Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Tue, 29 Sep 2026 00:21:52 +0200 Subject: [PATCH] Add insert() and erase() to editable documents - insert(array, index, value): insert before an element (index <= size) - erase(object, key): remove all members with the key; returns their number - erase(array, index): remove an element - erase(json_pointer): remove the member or element a pointer names The errors are those of basic_json (type_error.307/309, out_of_range.401/403/405). A view of an erased value keeps its last value, and views of other values keep referring to them when elements move. Tests: the differential test now also inserts and erases members and elements, directly and through JSON pointers; plus the errors, views across inserts and erasures, duplicate keys, and large objects. Signed-off-by: Niels Lohmann --- include/nlohmann/detail/view/edit.hpp | 56 ++++++++++++++++ include/nlohmann/json_view.hpp | 39 +++++++++++ single_include/nlohmann/json_view.hpp | 95 +++++++++++++++++++++++++++ tests/src/unit-json_view_edit.cpp | 72 +++++++++++++++++++- 4 files changed, 261 insertions(+), 1 deletion(-) diff --git a/include/nlohmann/detail/view/edit.hpp b/include/nlohmann/detail/view/edit.hpp index 6b22d986d..b83697618 100644 --- a/include/nlohmann/detail/view/edit.hpp +++ b/include/nlohmann/detail/view/edit.hpp @@ -227,6 +227,62 @@ class editor return View(&m_doc, slot); } + /// insert into an array before position idx (idx <= size()); returns a + /// view of the new element + template + View insert(const View& array, std::size_t idx, V&& value) + { + node* const a = own(array); + if (a->kind != static_cast(value_t::array)) + { + throw_type_error(309, "cannot use insert() with ", array.type_name()); + } + check_index(idx, a->len + 1); + const encoded e = encode(std::forward(value)); + node* const slot = new_slot(e); + node* const h = block_of(m_doc, a, 1); + std::memmove(h + 2 + idx, h + 1 + idx, (h->next - 1 - idx) * sizeof(node)); + make_link(h[1 + idx], slot); + ++h->next; + ++h->len; + ++a->len; + return View(&m_doc, slot); + } + + /// remove all members with this key; returns their number + std::size_t erase(const View& object, string_view_t key) + { + node* const o = own(object); + if (o->kind != static_cast(value_t::object)) + { + throw_type_error(307, "cannot use erase() with ", object.type_name()); + } + for (const node* k = nav::first(m_doc, o), *end = nav::end(m_doc, o); k != end; k = document_data::after(k + 1)) + { + if (key_equals(*k, key)) + { + return erase_members(o, key, false); + } + } + return 0; + } + + /// remove an array element + void erase(const View& array, std::size_t idx) + { + node* const a = own(array); + if (a->kind != static_cast(value_t::array)) + { + throw_type_error(307, "cannot use erase() with ", array.type_name()); + } + check_index(idx, a->len); + node* const h = block_of(m_doc, a, 0); + std::memmove(h + 1 + idx, h + 2 + idx, (h->next - 2 - idx) * sizeof(node)); + --h->next; + --h->len; + --a->len; + } + private: /// an encoded value: a scalar node, or the root of a new array/object struct encoded diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index a0c33a118..4a6b24832 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -1061,6 +1061,45 @@ class basic_json_document return editor().push_back(array, std::forward(value)); } + /// insert into an array before position idx (idx <= size()); returns a + /// view of the new element + template < typename I, typename V, typename std::enable_if < std::is_integral::value && !std::is_same::value, int >::type = 0 > + view_type insert(view_type array, I idx, V && value) + { + return editor().insert(array, index(idx), std::forward(value)); + } + + /// remove all members with this key; returns their number + std::size_t erase(view_type object, string_view_t key) + { + return editor().erase(object, key); + } + + /// remove an array element + template < typename I, typename std::enable_if < std::is_integral::value && !std::is_same::value, int >::type = 0 > + void erase(view_type array, I idx) + { + editor().erase(array, index(idx)); + } + + /// remove the value at a JSON pointer; returns the number of removed + /// values + std::size_t erase(const json_pointer& ptr) + { + if (ptr.empty()) + { + detail::view::throw_out_of_range(405, "JSON pointer has no parent"); + } + const view_type parent = root().at(ptr.parent_pointer()); + const auto& token = ptr.back(); + if (parent.is_array()) + { + erase(parent, pointer_index(token)); + return 1; + } + return erase(parent, string_view_t(token.data(), token.size())); + } + private: using input_kind = detail::view::input_kind; diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index dc8c17807..7f55ee693 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -3228,6 +3228,62 @@ class editor return View(&m_doc, slot); } + /// insert into an array before position idx (idx <= size()); returns a + /// view of the new element + template + View insert(const View& array, std::size_t idx, V&& value) + { + node* const a = own(array); + if (a->kind != static_cast(value_t::array)) + { + throw_type_error(309, "cannot use insert() with ", array.type_name()); + } + check_index(idx, a->len + 1); + const encoded e = encode(std::forward(value)); + node* const slot = new_slot(e); + node* const h = block_of(m_doc, a, 1); + std::memmove(h + 2 + idx, h + 1 + idx, (h->next - 1 - idx) * sizeof(node)); + make_link(h[1 + idx], slot); + ++h->next; + ++h->len; + ++a->len; + return View(&m_doc, slot); + } + + /// remove all members with this key; returns their number + std::size_t erase(const View& object, string_view_t key) + { + node* const o = own(object); + if (o->kind != static_cast(value_t::object)) + { + throw_type_error(307, "cannot use erase() with ", object.type_name()); + } + for (const node* k = nav::first(m_doc, o), *end = nav::end(m_doc, o); k != end; k = document_data::after(k + 1)) + { + if (key_equals(*k, key)) + { + return erase_members(o, key, false); + } + } + return 0; + } + + /// remove an array element + void erase(const View& array, std::size_t idx) + { + node* const a = own(array); + if (a->kind != static_cast(value_t::array)) + { + throw_type_error(307, "cannot use erase() with ", array.type_name()); + } + check_index(idx, a->len); + node* const h = block_of(m_doc, a, 0); + std::memmove(h + 1 + idx, h + 2 + idx, (h->next - 2 - idx) * sizeof(node)); + --h->next; + --h->len; + --a->len; + } + private: /// an encoded value: a scalar node, or the root of a new array/object struct encoded @@ -6240,6 +6296,45 @@ class basic_json_document return editor().push_back(array, std::forward(value)); } + /// insert into an array before position idx (idx <= size()); returns a + /// view of the new element + template < typename I, typename V, typename std::enable_if < std::is_integral::value && !std::is_same::value, int >::type = 0 > + view_type insert(view_type array, I idx, V && value) + { + return editor().insert(array, index(idx), std::forward(value)); + } + + /// remove all members with this key; returns their number + std::size_t erase(view_type object, string_view_t key) + { + return editor().erase(object, key); + } + + /// remove an array element + template < typename I, typename std::enable_if < std::is_integral::value && !std::is_same::value, int >::type = 0 > + void erase(view_type array, I idx) + { + editor().erase(array, index(idx)); + } + + /// remove the value at a JSON pointer; returns the number of removed + /// values + std::size_t erase(const json_pointer& ptr) + { + if (ptr.empty()) + { + detail::view::throw_out_of_range(405, "JSON pointer has no parent"); + } + const view_type parent = root().at(ptr.parent_pointer()); + const auto& token = ptr.back(); + if (parent.is_array()) + { + erase(parent, pointer_index(token)); + return 1; + } + return erase(parent, string_view_t(token.data(), token.size())); + } + private: using input_kind = detail::view::input_kind; diff --git a/tests/src/unit-json_view_edit.cpp b/tests/src/unit-json_view_edit.cpp index 7d012877d..cca222b09 100644 --- a/tests/src/unit-json_view_edit.cpp +++ b/tests/src/unit-json_view_edit.cpp @@ -278,12 +278,45 @@ TEST_CASE("json_view edits: differential") 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 std::size_t 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 std::size_t 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()))); @@ -350,6 +383,13 @@ TEST_CASE("json_view edits: errors") 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&); @@ -390,6 +430,27 @@ TEST_CASE("json_view edits: views and values") 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("[]"); @@ -433,6 +494,10 @@ TEST_CASE("json_view edits: views and values") 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") @@ -440,6 +505,9 @@ TEST_CASE("json_view edits: views and values") 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") @@ -472,7 +540,9 @@ TEST_CASE("json_view edits: views and values") 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); - CHECK(d.root().size() == 201); + d.erase(d.root(), "k0"); + CHECK(!d.root().contains("k0")); + CHECK(d.root().size() == 200); } SECTION("reuse and memory")