Compare commits

...
Author SHA1 Message Date
Niels Lohmann 412d82f6c7 Document the one-character array index change
Add 3.13.0 version-history entries to at, operator[], value, patch,
patch_inplace, and unflatten, and describe in exceptions.md which array
indices throw parse_error.109 and which out_of_range.404.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-10 12:46:12 +02:00
Niels Lohmann 26b2494aa7 Report one-character non-numeric array indices like longer ones
A JSON pointer reference token that is not a number but has only one
character (e.g. "/a/x") was reported as out_of_range.404 ("unresolved
reference token"), because the "is not a number" check only ran for
tokens longer than one character; "/a/xy" got parse_error.109. Both now
throw parse_error.109. "-" and the empty token are still reported as
out_of_range.404. As a consequence, value(json_pointer, default) on an
array now throws for "/x" as it already did for "/xy".

Also document why ordered_map::erase's destroy/placement-new loop on
pair<const Key, T> is kept despite [basic.life]/8 before C++20.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-10 12:42:06 +02:00
13 changed files with 95 additions and 13 deletions

No files matched your search

+3 -1
View File
@@ -241,4 +241,6 @@ Strong exception safety: if an exception occurs, the original value stays intact
3. Added in version 3.11.0. Fixed in version 3.13.0 to consistently accept `std::string_view`-convertible keys, as 3. Added in version 3.11.0. Fixed in version 3.13.0 to consistently accept `std::string_view`-convertible keys, as
already supported by [`operator[]`](operator[].md), [`value`](value.md), [`find`](find.md), and other lookup already supported by [`operator[]`](operator[].md), [`value`](value.md), [`find`](find.md), and other lookup
functions. functions.
4. Added in version 2.0.0. 4. Added in version 2.0.0. Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) instead of
[`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) for a one-character array index that is not
a digit (e.g., `/x`) in version 3.13.0, as it already did for longer ones.
@@ -286,4 +286,6 @@ Strong exception safety: if an exception occurs, the original value stays intact
3. Added in version 3.11.0. Fixed in version 3.13.0 to consistently accept `std::string_view`-convertible keys, as 3. Added in version 3.11.0. Fixed in version 3.13.0 to consistently accept `std::string_view`-convertible keys, as
already supported by [`at`](at.md), [`value`](value.md), [`find`](find.md), and other lookup functions. already supported by [`at`](at.md), [`value`](value.md), [`find`](find.md), and other lookup functions.
4. Added in version 2.0.0. A missing array index in the const version is guarded by a runtime assertion since 4. Added in version 2.0.0. A missing array index in the const version is guarded by a runtime assertion since
version 3.13.0. version 3.13.0. Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) instead of
[`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) for a one-character array index that is not
a digit (e.g., `/x`) in version 3.13.0, as it already did for longer ones.
+3
View File
@@ -112,3 +112,6 @@ is thrown. In any case, the original value is not changed: the patch is applied
location has a non-object/non-array parent in version 3.13.0. location has a non-object/non-array parent in version 3.13.0.
- Added [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) and rejected a "move" operation whose "from" location is a proper - Added [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) and rejected a "move" operation whose "from" location is a proper
prefix of its "path" location instead of silently producing a corrupted result in version 3.13.0. prefix of its "path" location instead of silently producing a corrupted result in version 3.13.0.
- Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) instead of
[`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) for a one-character array index that is
not a digit (e.g., `/x`) in version 3.13.0, as it already did for longer ones.
@@ -110,3 +110,6 @@ function throws an exception.
location has a non-object/non-array parent in version 3.13.0. location has a non-object/non-array parent in version 3.13.0.
- Added [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) and rejected a "move" operation whose "from" location is a proper - Added [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) and rejected a "move" operation whose "from" location is a proper
prefix of its "path" location instead of silently producing a corrupted result in version 3.13.0. prefix of its "path" location instead of silently producing a corrupted result in version 3.13.0.
- Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) instead of
[`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) for a one-character array index that is
not a digit (e.g., `/x`) in version 3.13.0, as it already did for longer ones.
+6 -2
View File
@@ -36,8 +36,9 @@ The function can throw the following exceptions:
- Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) if an array index in a key is not a - Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) if an array index in a key is not a
number; example: `"array index 'one' is not a number"` number; example: `"array index 'one' is not a number"`
- Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if a level becomes an array - Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if a level becomes an array
(because one of its keys is `0`) and another key at that level cannot be an array index; example: (because one of its keys is `0`) and another key at that level begins with a digit but is not a valid array index
`"unresolved reference token 'x'"` (such as `1a`), or is `-`; example:
`"unresolved reference token '-'"`
## Complexity ## Complexity
@@ -80,3 +81,6 @@ Apart from these two cases, for a JSON value `j`, the following is always true:
- Added in version 2.0.0. - Added in version 2.0.0.
- Made the array/object decision independent of the object's iteration order in version 3.13.0. - Made the array/object decision independent of the object's iteration order in version 3.13.0.
- Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) instead of
[`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) for a one-character array index that is
not a digit (e.g., `/x`) in version 3.13.0, as it already did for longer ones.
+3 -1
View File
@@ -227,4 +227,6 @@ changes to any JSON value.
[`operator[]`](operator[].md), [`at`](at.md), [`find`](find.md), and other lookup functions. [`operator[]`](operator[].md), [`at`](at.md), [`find`](find.md), and other lookup functions.
3. Added in version 2.0.2. Extended to work with arrays in version 3.13.0, including fixing an issue where resolving 3. Added in version 2.0.2. Extended to work with arrays in version 3.13.0, including fixing an issue where resolving
`ptr` through an array unexpectedly threw `out_of_range` instead of returning the resolved element (or `ptr` through an array unexpectedly threw `out_of_range` instead of returning the resolved element (or
`default_value`, as documented). `default_value`, as documented). Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109)
instead of returning `default_value` for a one-character array index that is not a digit (e.g., `/x`) in version
3.13.0, as it already did for longer ones.
+10 -2
View File
@@ -278,7 +278,9 @@ In a JSON Pointer, only `~0` and `~1` are valid escape sequences.
### json.exception.parse_error.109 ### json.exception.parse_error.109
A JSON Pointer array index must be a number. A JSON Pointer array index must be a number. This exception is thrown for an array index that does not begin with a
digit, except for `-` and the empty reference token, which throw [`out_of_range.404`](#jsonexceptionout_of_range404)
where they cannot be resolved.
!!! failure "Example messages" !!! failure "Example messages"
@@ -289,6 +291,11 @@ A JSON Pointer array index must be a number.
[json.exception.parse_error.109] parse error: array index '+1' is not a number [json.exception.parse_error.109] parse error: array index '+1' is not a number
``` ```
!!! note
Before version 3.13.0, a one-character array index that is not a digit (e.g., `x`) threw
[`out_of_range.404`](#jsonexceptionout_of_range404) instead.
### json.exception.parse_error.110 ### json.exception.parse_error.110
When parsing a [binary format](../features/binary_formats/index.md), the byte vector ends before the complete value has When parsing a [binary format](../features/binary_formats/index.md), the byte vector ends before the complete value has
@@ -871,7 +878,8 @@ The provided key was not found in the JSON object.
### json.exception.out_of_range.404 ### json.exception.out_of_range.404
A reference token in a JSON Pointer could not be resolved. A reference token in a JSON Pointer could not be resolved, for instance an array index that begins with a digit but
contains other characters (e.g., `1a`), or `-` where it cannot be used.
!!! failure "Example message" !!! failure "Example message"
+5 -3
View File
@@ -255,7 +255,7 @@ class json_pointer
{ {
ok, ///< @a s is a valid, representable array index ok, ///< @a s is a valid, representable array index
leading_zero, ///< @a s begins with '0' but has more than one character leading_zero, ///< @a s begins with '0' but has more than one character
not_a_number, ///< @a s does not begin with a digit not_a_number, ///< @a s is neither empty nor "-" and does not begin with a digit
unresolved, ///< @a s could not be converted to an integer unresolved, ///< @a s could not be converted to an integer
exceeds_size_type ///< @a s converts to an integer that exceeds size_type exceeds_size_type ///< @a s converts to an integer that exceeds size_type
}; };
@@ -282,8 +282,10 @@ class json_pointer
return array_index_status::leading_zero; return array_index_status::leading_zero;
} }
// error condition (cf. RFC 6901, Sect. 4) // error condition (cf. RFC 6901, Sect. 4); this also covers single-
if (JSON_HEDLEY_UNLIKELY(s.size() > 1 && !(s[0] >= '1' && s[0] <= '9'))) // character tokens, so "/x" and "/xy" fail alike; "-" and the empty
// token are left to the conversion below and are reported as unresolved
if (JSON_HEDLEY_UNLIKELY(!s.empty() && s != "-" && !(s[0] >= '0' && s[0] <= '9')))
{ {
return array_index_status::not_a_number; return array_index_status::not_a_number;
} }
+8
View File
@@ -247,6 +247,14 @@ public:
// ^ ^ // ^ ^
// first last // first last
// Note on conformance: before C++20, [basic.life]/8 did not allow an
// object of a type with a const member (like value_type's const Key)
// to transparently replace the destroyed one, so strictly, accessing
// it through the vector's existing pointers would have required
// std::launder (which does not exist before C++17). C++20 dropped that
// condition (P1971R0, NB comment US 041). Compilers have always treated
// this pattern as intended, so it is kept deliberately.
// Since we cannot move const Keys, we re-construct them in place. // Since we cannot move const Keys, we re-construct them in place.
// We start at first and re-construct (viz. copy) the elements from // We start at first and re-construct (viz. copy) the elements from
// the back of the vector. Example for the first iteration: // the back of the vector. Example for the first iteration:
+13 -3
View File
@@ -20173,7 +20173,7 @@ class json_pointer
{ {
ok, ///< @a s is a valid, representable array index ok, ///< @a s is a valid, representable array index
leading_zero, ///< @a s begins with '0' but has more than one character leading_zero, ///< @a s begins with '0' but has more than one character
not_a_number, ///< @a s does not begin with a digit not_a_number, ///< @a s is neither empty nor "-" and does not begin with a digit
unresolved, ///< @a s could not be converted to an integer unresolved, ///< @a s could not be converted to an integer
exceeds_size_type ///< @a s converts to an integer that exceeds size_type exceeds_size_type ///< @a s converts to an integer that exceeds size_type
}; };
@@ -20200,8 +20200,10 @@ class json_pointer
return array_index_status::leading_zero; return array_index_status::leading_zero;
} }
// error condition (cf. RFC 6901, Sect. 4) // error condition (cf. RFC 6901, Sect. 4); this also covers single-
if (JSON_HEDLEY_UNLIKELY(s.size() > 1 && !(s[0] >= '1' && s[0] <= '9'))) // character tokens, so "/x" and "/xy" fail alike; "-" and the empty
// token are left to the conversion below and are reported as unresolved
if (JSON_HEDLEY_UNLIKELY(!s.empty() && s != "-" && !(s[0] >= '0' && s[0] <= '9')))
{ {
return array_index_status::not_a_number; return array_index_status::not_a_number;
} }
@@ -27780,6 +27782,14 @@ public:
// ^ ^ // ^ ^
// first last // first last
// Note on conformance: before C++20, [basic.life]/8 did not allow an
// object of a type with a const member (like value_type's const Key)
// to transparently replace the destroyed one, so strictly, accessing
// it through the vector's existing pointers would have required
// std::launder (which does not exist before C++17). C++20 dropped that
// condition (P1971R0, NB comment US 041). Compilers have always treated
// this pattern as intended, so it is kept deliberately.
// Since we cannot move const Keys, we re-construct them in place. // Since we cannot move const Keys, we re-construct them in place.
// We start at first and re-construct (viz. copy) the elements from // We start at first and re-construct (viz. copy) the elements from
// the back of the vector. Example for the first iteration: // the back of the vector. Example for the first iteration:
+2
View File
@@ -535,6 +535,8 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j
// Test malformed index (non-numeric) throws parse_error // Test malformed index (non-numeric) throws parse_error
CHECK_THROWS_WITH_AS(j_array.value("/foo"_json_pointer, 1), "[json.exception.parse_error.109] parse error: array index 'foo' is not a number", typename Json::parse_error&); CHECK_THROWS_WITH_AS(j_array.value("/foo"_json_pointer, 1), "[json.exception.parse_error.109] parse error: array index 'foo' is not a number", typename Json::parse_error&);
CHECK_THROWS_WITH_AS(j_array_const.value("/foo"_json_pointer, 1), "[json.exception.parse_error.109] parse error: array index 'foo' is not a number", typename Json::parse_error&); CHECK_THROWS_WITH_AS(j_array_const.value("/foo"_json_pointer, 1), "[json.exception.parse_error.109] parse error: array index 'foo' is not a number", typename Json::parse_error&);
CHECK_THROWS_WITH_AS(j_array.value("/x"_json_pointer, 1), "[json.exception.parse_error.109] parse error: array index 'x' is not a number", typename Json::parse_error&);
CHECK_THROWS_WITH_AS(j_array_const.value("/x"_json_pointer, 1), "[json.exception.parse_error.109] parse error: array index 'x' is not a number", typename Json::parse_error&);
// Test leading-zero index throws parse_error // Test leading-zero index throws parse_error
CHECK_THROWS_WITH_AS(j_array.value("/01"_json_pointer, 1), "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", typename Json::parse_error&); CHECK_THROWS_WITH_AS(j_array.value("/01"_json_pointer, 1), "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", typename Json::parse_error&);
+15
View File
@@ -1713,6 +1713,21 @@ TEST_CASE("JSON patch - move where 'from' is a proper prefix of 'path' (regressi
CHECK(doc.patch(patch) == R"({"b": 1})"_json); CHECK(doc.patch(patch) == R"({"b": 1})"_json);
} }
SECTION("array index tokens that are not a number")
{
json const doc = R"({"a": [1, 2]})"_json;
// "-" cannot be removed and is reported as unresolved
json const patch_dash = {{{"op", "remove"}, {"path", "/a/-"}}};
CHECK_THROWS_WITH_AS(doc.patch(patch_dash), "[json.exception.out_of_range.404] unresolved reference token '-'", json::out_of_range&);
// a single character is reported like a longer token
json const patch_x = {{{"op", "remove"}, {"path", "/a/x"}}};
CHECK_THROWS_WITH_AS(doc.patch(patch_x), "[json.exception.parse_error.109] parse error: array index 'x' is not a number", json::parse_error&);
json const patch_xy = {{{"op", "remove"}, {"path", "/a/xy"}}};
CHECK_THROWS_WITH_AS(doc.patch(patch_xy), "[json.exception.parse_error.109] parse error: array index 'xy' is not a number", json::parse_error&);
}
SECTION("the array-append token '-' is an ordinary child token") SECTION("the array-append token '-' is an ordinary child token")
{ {
// "-" (append-to-array) addresses a location *inside* the array, // "-" (append-to-array) addresses a location *inside* the array,
+21
View File
@@ -421,6 +421,27 @@ TEST_CASE("JSON pointers")
CHECK_THROWS_WITH_AS(json({{"/list/0", 1}, {"/list/1", 2}, {"/list/three", 3}}).unflatten(), CHECK_THROWS_WITH_AS(json({{"/list/0", 1}, {"/list/1", 2}, {"/list/three", 3}}).unflatten(),
"[json.exception.parse_error.109] parse error: array index 'three' is not a number", json::parse_error&); "[json.exception.parse_error.109] parse error: array index 'three' is not a number", json::parse_error&);
// a single-character token that is not a digit is reported like a
// longer one (parse_error.109), not as unresolved (out_of_range.404)
CHECK_THROWS_WITH_AS(j["/x"_json_pointer] = 1,
"[json.exception.parse_error.109] parse error: array index 'x' is not a number", json::parse_error&);
CHECK_THROWS_WITH_AS(j_const["/x"_json_pointer] == 1,
"[json.exception.parse_error.109] parse error: array index 'x' is not a number", json::parse_error&);
CHECK_THROWS_WITH_AS(j.at("/x"_json_pointer) = 1,
"[json.exception.parse_error.109] parse error: array index 'x' is not a number", json::parse_error&);
CHECK_THROWS_WITH_AS(j_const.at("/x"_json_pointer) == 1,
"[json.exception.parse_error.109] parse error: array index 'x' is not a number", json::parse_error&);
CHECK_THROWS_WITH_AS(j.at("/+"_json_pointer),
"[json.exception.parse_error.109] parse error: array index '+' is not a number", json::parse_error&);
CHECK(!j.contains("/x"_json_pointer));
CHECK(!j_const.contains("/x"_json_pointer));
CHECK_THROWS_WITH_AS(json({{"/list/0", 1}, {"/list/x", 2}}).unflatten(),
"[json.exception.parse_error.109] parse error: array index 'x' is not a number", json::parse_error&);
// "-" is a valid reference token, so it is still reported as unresolved
CHECK_THROWS_WITH_AS(json({{"/list/0", 1}, {"/list/-", 2}}).unflatten(),
"[json.exception.out_of_range.404] unresolved reference token '-'", json::out_of_range&);
// assign to "-" // assign to "-"
j["/-"_json_pointer] = 99; j["/-"_json_pointer] = 99;
CHECK(j == json({1, 13, 3, 33, nullptr, 55, 99})); CHECK(j == json({1, 13, 3, 33, nullptr, 55, 99}));