Skip to content

Commit da15b59

Browse files
committed
create objects for tokens that cannot be array indices (#5357)
When operator[](json_pointer) traverses a level that does not exist yet, the null value is turned into an array or an object depending on the reference token. The check only tested whether all characters are digits, so tokens that can never be a valid array index selected an array and then failed: - "01" (and any other token with a leading '0') threw parse_error.106 - the empty token threw out_of_range.404 Both tokens are valid object keys, and both work when the level already exists as an object, so creating the level changed the outcome. Test the token against the RFC 6901, Sect. 4 grammar for array indices instead, so that such tokens create an object. This only affects pointers that threw before. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
1 parent 78821cd commit da15b59

4 files changed

Lines changed: 91 additions & 14 deletions

File tree

docs/mkdocs/docs/api/basic_json/operator[].md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,13 @@ Strong exception safety: if an exception occurs, the original value stays intact
128128

129129
When the JSON pointer traverses intermediate levels that don't exist at all yet (not just a missing
130130
leaf), each missing level is created as an array or an object depending on whether the corresponding
131-
pointer token parses as a non-negative integer: a numeric token creates an array, a non-numeric token
132-
creates an object. For example, on an initially `#!json null` value, `/foo/0/0/0` creates nested arrays,
133-
while `/foo/one/one/one` creates nested objects. This is not specified by the JSON Pointer RFC; it is
134-
this library's own, intentional disambiguation rule. See also [JSON Pointer](../../features/json_pointer.md).
131+
pointer token is a valid array index: a token that is a nonempty sequence of digits without a leading
132+
`0` (or the token `-`) creates an array, and every other token creates an object. For example, on an
133+
initially `#!json null` value, `/foo/0/0/0` creates nested arrays, while `/foo/one/one/one` creates
134+
nested objects. Tokens such as `01` or the empty token cannot be array indices (cf. RFC 6901, Sect. 4)
135+
and therefore create objects, just as they would if the level already existed as an object. This is not
136+
specified by the JSON Pointer RFC; it is this library's own, intentional disambiguation rule. See also
137+
[JSON Pointer](../../features/json_pointer.md).
135138

136139
## Examples
137140

include/nlohmann/detail/json_pointer.hpp

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -396,15 +396,19 @@ class json_pointer
396396
// convert null values to arrays or objects before continuing
397397
if (ptr->is_null())
398398
{
399-
// check if the reference token is a number
400-
const bool nums =
401-
std::all_of(reference_token.begin(), reference_token.end(),
402-
[](const unsigned char x)
399+
// check if the reference token is a valid array index, that is
400+
// a nonempty sequence of digits without a leading '0'
401+
// (cf. RFC 6901, Sect. 4); tokens that could never be a valid
402+
// array index (such as "01" or "") are treated as object keys
403+
const bool nums = !reference_token.empty()
404+
&& (reference_token.size() == 1 || reference_token[0] != '0')
405+
&& std::all_of(reference_token.begin(), reference_token.end(),
406+
[](const unsigned char x)
403407
{
404408
return std::isdigit(x);
405409
});
406410

407-
// change value to an array for numbers or "-" or to object otherwise
411+
// change value to an array for array indices or "-" or to object otherwise
408412
*ptr = (nums || reference_token == "-")
409413
? detail::value_t::array
410414
: detail::value_t::object;

single_include/nlohmann/json.hpp

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15937,15 +15937,19 @@ class json_pointer
1593715937
// convert null values to arrays or objects before continuing
1593815938
if (ptr->is_null())
1593915939
{
15940-
// check if the reference token is a number
15941-
const bool nums =
15942-
std::all_of(reference_token.begin(), reference_token.end(),
15943-
[](const unsigned char x)
15940+
// check if the reference token is a valid array index, that is
15941+
// a nonempty sequence of digits without a leading '0'
15942+
// (cf. RFC 6901, Sect. 4); tokens that could never be a valid
15943+
// array index (such as "01" or "") are treated as object keys
15944+
const bool nums = !reference_token.empty()
15945+
&& (reference_token.size() == 1 || reference_token[0] != '0')
15946+
&& std::all_of(reference_token.begin(), reference_token.end(),
15947+
[](const unsigned char x)
1594415948
{
1594515949
return std::isdigit(x);
1594615950
});
1594715951

15948-
// change value to an array for numbers or "-" or to object otherwise
15952+
// change value to an array for array indices or "-" or to object otherwise
1594915953
*ptr = (nums || reference_token == "-")
1595015954
? detail::value_t::array
1595115955
: detail::value_t::object;

tests/src/unit-json_pointer.cpp

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,72 @@ TEST_CASE("JSON pointers")
396396
}
397397
}
398398

399+
SECTION("creating intermediate levels")
400+
{
401+
SECTION("tokens that are valid array indices create arrays")
402+
{
403+
json j;
404+
j["/0"_json_pointer] = 1;
405+
CHECK(j == json({1}));
406+
407+
json j2;
408+
j2["/2"_json_pointer] = 1;
409+
CHECK(j2 == json({nullptr, nullptr, 1}));
410+
411+
json j3;
412+
j3["/-"_json_pointer] = 1;
413+
CHECK(j3 == json({1}));
414+
415+
json j4;
416+
j4["/foo/0/0"_json_pointer] = 1;
417+
CHECK(j4 == json({{"foo", {{1}}}}));
418+
}
419+
420+
SECTION("tokens that are no valid array indices create objects")
421+
{
422+
json j;
423+
j["/one"_json_pointer] = 1;
424+
CHECK(j == json({{"one", 1}}));
425+
426+
// leading '0' can never be a valid array index (RFC 6901, Sect. 4)
427+
json j2;
428+
j2["/01"_json_pointer] = 1;
429+
CHECK(j2 == json({{"01", 1}}));
430+
431+
// the empty token is a valid object key, but no valid array index
432+
json j3;
433+
j3["/"_json_pointer] = 1;
434+
CHECK(j3 == json({{"", 1}}));
435+
}
436+
437+
SECTION("creating a level yields the same result as reusing it (#5357)")
438+
{
439+
json j;
440+
j["/a/b/01/d"_json_pointer] = "value";
441+
442+
json j_init = json::object();
443+
j_init["/a/b"_json_pointer] = json::object();
444+
j_init["/a/b/01/d"_json_pointer] = "value";
445+
446+
const json expected = json::parse(R"({"a":{"b":{"01":{"d":"value"}}}})");
447+
CHECK(j == expected);
448+
CHECK(j_init == expected);
449+
450+
// unflatten uses the same key
451+
const json flat = {{"/a/b/01/d", "value"}};
452+
CHECK(flat.unflatten() == expected);
453+
}
454+
455+
SECTION("existing arrays still reject invalid indices")
456+
{
457+
json j = {1, 2, 3};
458+
CHECK_THROWS_WITH_AS(j["/01"_json_pointer],
459+
"[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
460+
CHECK_THROWS_WITH_AS(j.at("/01"_json_pointer),
461+
"[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
462+
}
463+
}
464+
399465
SECTION("flatten")
400466
{
401467
json j =

0 commit comments

Comments
 (0)