From 79a51feb44dccb04b6472558f0056c297ead74ec Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 9 Oct 2026 16:29:54 +0200 Subject: [PATCH] Set a null value below a pointer as basic_json does set(json_pointer, value) turned a null parent into an object for every last reference token, so "/a/0" and "/a/-" below {"a":null} made {"a":{"0":1}}, where basic_json's operator[](json_pointer) makes an array. A null parent now becomes an array for "-" and for digit tokens (filled with nulls up to the index) and an object otherwise. An invalid index is reported before the parent changes. Signed-off-by: Niels Lohmann --- .../docs/api/basic_json_document/set.md | 16 ++++-- include/nlohmann/json_view.hpp | 30 +++++++++- tests/src/unit-json_view_edit.cpp | 55 +++++++++++++++++++ 3 files changed, 95 insertions(+), 6 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json_document/set.md b/docs/mkdocs/docs/api/basic_json_document/set.md index f9d73b94c..067fa9fef 100644 --- a/docs/mkdocs/docs/api/basic_json_document/set.md +++ b/docs/mkdocs/docs/api/basic_json_document/set.md @@ -31,7 +31,10 @@ has `set`; calling it on a read-only `basic_json_document` fails to compile (`#! 4. Sets the value the JSON pointer `ptr` refers to, relative to [`root()`](root.md), to `value`. The *parent* of the target must already exist: an object member is set as in 2. (added if it does not exist yet), an array element is assigned as in 3., and a last reference token of `#!cpp "-"`, or equal to the size of the array, appends `value` - instead, exactly as [`push_back`](push_back.md) would. An empty `ptr` sets [`root()`](root.md) itself, as in 1. + instead, exactly as [`push_back`](push_back.md) would. A [null](../basic_json_view/is_null.md) parent becomes what + [`basic_json::operator[]`](../basic_json/operator%5B%5D.md) with a JSON pointer makes of it: an array if the last + reference token is `#!cpp "-"` or consists of digits only (for an index beyond 0, the array is first filled with + null values up to that index), an object otherwise. An empty `ptr` sets [`root()`](root.md) itself, as in 1. In every overload, `value` is accepted three ways: a [`basic_json_view`](../basic_json_view/index.md) of *any* document -- read-only or editable, and it does not have to be `target`'s/`object`'s/`array`'s own document -- which @@ -85,7 +88,8 @@ invalid argument, or `#!cpp std::bad_alloc`) leaves the document completely unch for the encoding that is not reclaimed. A failure of a later allocation -- while an edited array or object switches from its parsed layout to a growable block, see [Notes](#notes) -- can still leave a partial effect, such as a [null](../basic_json_view/is_null.md) `object`/`array` argument already turned into an empty object/array even -though `value` itself was not linked in. +though `value` itself was not linked in. Likewise, a failure of `value` in 4. leaves a null parent that is set with an +index beyond 0 already filled with the null values before the index. ## Exceptions @@ -111,8 +115,10 @@ though `value` itself was not linked in. [`parse_error.106`](../../home/exceptions.md#jsonexceptionparse_error106) (a leading `#!cpp '0'`), [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) (not a number), [`out_of_range.410`](../../home/exceptions.md#jsonexceptionout_of_range410) (too large for `size_type`), or - [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) (an empty token). Also throws what 1. - throws for `value`. + [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) (an empty token); the same errors are + thrown for a null parent and a token of digits (the parent is not changed then), and + [`out_of_range.401`](../../home/exceptions.md#jsonexceptionout_of_range401) if the index is 4294967295 or more. + Also throws what 1. throws for `value`. Every overload also throws [`type_error.319`](../../home/exceptions.md#jsonexceptiontype_error319) if `value` is (or contains) a binary value -- `BasicJsonType` can hold one, but a `json_document` cannot -- and @@ -131,7 +137,7 @@ document") if `target`/`object`/`array` is a [discarded](../basic_json_view/is_d 3. Constant, plus the complexity of 1. for `value`. 4. Linear in the number of reference tokens of `ptr` and, for each token, in the number of members of the object at that level or the index into the array (as [`at`](../basic_json_view/at.md)), plus the complexity of 2. or 3. for - the last token. + the last token; for a null parent and an index, linear in the index. ## Notes diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index 03ba3fb64..a0c7f5ba4 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -24,6 +24,7 @@ #ifndef INCLUDE_NLOHMANN_JSON_VIEW_HPP_ #define INCLUDE_NLOHMANN_JSON_VIEW_HPP_ +#include // all_of #include // size_t #include // uint32_t #include // memcpy, strlen @@ -1040,7 +1041,9 @@ class basic_json_document /// set the value at a JSON pointer: its parent must exist; an object /// member is set (added if missing), an array element assigned, and "-" - /// or the size of the array appends + /// or the size of the array appends. A null parent becomes what + /// basic_json's operator[](json_pointer) makes of it: an array for "-" + /// and for digits (padded with nulls up to the index), an object otherwise. template view_type set(const json_pointer& ptr, V&& value) { @@ -1050,6 +1053,31 @@ class basic_json_document } const view_type parent = root().at(ptr.parent_pointer()); const auto& token = ptr.back(); + if (parent.is_null()) + { + const bool digits = std::all_of(token.begin(), token.end(), [](const char c) + { + return c >= '0' && c <= '9'; + }); + if (token == "-") + { + return push_back(parent, std::forward(value)); + } + if (digits) + { + // (an invalid index is an error before the parent changes) + const std::size_t idx = pointer_index(token); + if (idx >= 0xFFFFFFFFu) + { + detail::view::throw_out_of_range(401, detail::concat("array index ", std::to_string(idx), " is out of range")); + } + for (std::size_t i = 0; i < idx; ++i) + { + push_back(parent, nullptr); + } + return push_back(parent, std::forward(value)); + } + } if (parent.is_array()) { const std::size_t idx = token == "-" ? parent.size() : pointer_index(token); diff --git a/tests/src/unit-json_view_edit.cpp b/tests/src/unit-json_view_edit.cpp index 3e6d00c19..4116462b5 100644 --- a/tests/src/unit-json_view_edit.cpp +++ b/tests/src/unit-json_view_edit.cpp @@ -634,6 +634,61 @@ TEST_CASE("json_view edits: deeply nested values") } #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