diff --git a/docs/mkdocs/docs/api/basic_json/operator[].md b/docs/mkdocs/docs/api/basic_json/operator[].md index 3195870e4..bcb6ddc77 100644 --- a/docs/mkdocs/docs/api/basic_json/operator[].md +++ b/docs/mkdocs/docs/api/basic_json/operator[].md @@ -128,10 +128,13 @@ Strong exception safety: if an exception occurs, the original value stays intact When the JSON pointer traverses intermediate levels that don't exist at all yet (not just a missing leaf), each missing level is created as an array or an object depending on whether the corresponding - pointer token parses as a non-negative integer: a numeric token creates an array, a non-numeric token - creates an object. For example, on an initially `#!json null` value, `/foo/0/0/0` creates nested arrays, - while `/foo/one/one/one` creates nested objects. This is not specified by the JSON Pointer RFC; it is - this library's own, intentional disambiguation rule. See also [JSON Pointer](../../features/json_pointer.md). + pointer token is a valid array index: a token that is a nonempty sequence of digits without a leading + `0` (or the token `-`) creates an array, and every other token creates an object. For example, on an + initially `#!json null` value, `/foo/0/0/0` creates nested arrays, while `/foo/one/one/one` creates + nested objects. Tokens such as `01` or the empty token cannot be array indices (cf. RFC 6901, Sect. 4) + and therefore create objects, just as they would if the level already existed as an object. This is not + specified by the JSON Pointer RFC; it is this library's own, intentional disambiguation rule. See also + [JSON Pointer](../../features/json_pointer.md). ## Examples diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index 576ba62cc..425d547c8 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -396,15 +396,19 @@ class json_pointer // convert null values to arrays or objects before continuing if (ptr->is_null()) { - // check if the reference token is a number - const bool nums = - std::all_of(reference_token.begin(), reference_token.end(), - [](const unsigned char x) + // check if the reference token is a valid array index, that is + // a nonempty sequence of digits without a leading '0' + // (cf. RFC 6901, Sect. 4); tokens that could never be a valid + // array index (such as "01" or "") are treated as object keys + const bool nums = !reference_token.empty() + && (reference_token.size() == 1 || reference_token[0] != '0') + && std::all_of(reference_token.begin(), reference_token.end(), + [](const unsigned char x) { return std::isdigit(x); }); - // change value to an array for numbers or "-" or to object otherwise + // change value to an array for array indices or "-" or to object otherwise *ptr = (nums || reference_token == "-") ? detail::value_t::array : detail::value_t::object; diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 958ac5fc9..55375aa00 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -15937,15 +15937,19 @@ class json_pointer // convert null values to arrays or objects before continuing if (ptr->is_null()) { - // check if the reference token is a number - const bool nums = - std::all_of(reference_token.begin(), reference_token.end(), - [](const unsigned char x) + // check if the reference token is a valid array index, that is + // a nonempty sequence of digits without a leading '0' + // (cf. RFC 6901, Sect. 4); tokens that could never be a valid + // array index (such as "01" or "") are treated as object keys + const bool nums = !reference_token.empty() + && (reference_token.size() == 1 || reference_token[0] != '0') + && std::all_of(reference_token.begin(), reference_token.end(), + [](const unsigned char x) { return std::isdigit(x); }); - // change value to an array for numbers or "-" or to object otherwise + // change value to an array for array indices or "-" or to object otherwise *ptr = (nums || reference_token == "-") ? detail::value_t::array : detail::value_t::object; diff --git a/tests/src/unit-json_pointer.cpp b/tests/src/unit-json_pointer.cpp index a8ed4a89e..4b3f8f52c 100644 --- a/tests/src/unit-json_pointer.cpp +++ b/tests/src/unit-json_pointer.cpp @@ -396,6 +396,72 @@ TEST_CASE("JSON pointers") } } + SECTION("creating intermediate levels") + { + SECTION("tokens that are valid array indices create arrays") + { + json j; + j["/0"_json_pointer] = 1; + CHECK(j == json({1})); + + json j2; + j2["/2"_json_pointer] = 1; + CHECK(j2 == json({nullptr, nullptr, 1})); + + json j3; + j3["/-"_json_pointer] = 1; + CHECK(j3 == json({1})); + + json j4; + j4["/foo/0/0"_json_pointer] = 1; + CHECK(j4 == json({{"foo", {{1}}}})); + } + + SECTION("tokens that are no valid array indices create objects") + { + json j; + j["/one"_json_pointer] = 1; + CHECK(j == json({{"one", 1}})); + + // leading '0' can never be a valid array index (RFC 6901, Sect. 4) + json j2; + j2["/01"_json_pointer] = 1; + CHECK(j2 == json({{"01", 1}})); + + // the empty token is a valid object key, but no valid array index + json j3; + j3["/"_json_pointer] = 1; + CHECK(j3 == json({{"", 1}})); + } + + SECTION("creating a level yields the same result as reusing it (#5357)") + { + json j; + j["/a/b/01/d"_json_pointer] = "value"; + + json j_init = json::object(); + j_init["/a/b"_json_pointer] = json::object(); + j_init["/a/b/01/d"_json_pointer] = "value"; + + const json expected = json::parse(R"({"a":{"b":{"01":{"d":"value"}}}})"); + CHECK(j == expected); + CHECK(j_init == expected); + + // unflatten uses the same key + const json flat = {{"/a/b/01/d", "value"}}; + CHECK(flat.unflatten() == expected); + } + + SECTION("existing arrays still reject invalid indices") + { + json j = {1, 2, 3}; + CHECK_THROWS_WITH_AS(j["/01"_json_pointer], + "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&); + CHECK_THROWS_WITH_AS(j.at("/01"_json_pointer), + "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&); + } + } + SECTION("flatten") { json j =