Compare commits

..
Author SHA1 Message Date
Niels Lohmann b15ec08275 Merge branch 'develop' into claude/const-json-pointer-assertion
Conflicts:
- docs/mkdocs/docs/api/basic_json/operator[].md: version history item 1 keeps develop's std::length_error fix note and appends the PR's runtime assertion note

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-30 20:42:02 +02:00
Niels Lohmann 93fe62fc0b Assert on missing array indices in const operator[] and document the JSON pointer case
The const operator[] overloads are unchecked by design, and a missing key
or index is undefined behavior. The key overload guards this with a
runtime assertion, but the index overload did not, although the element
access documentation says an assertion fires in both cases. The const
JSON pointer overload inherits both through json_pointer::get_unchecked(),
so a pointer to a missing array index read out of bounds even in debug
builds, and its documentation promised out_of_range.404 for any pointer
that cannot be resolved.

Add JSON_ASSERT(idx < size()) to const operator[](size_type), which also
covers the index leg of the const JSON pointer overload. Document the
undefined behavior for the const JSON pointer overload in operator[].md
and in the runtime assertions page. Release builds are unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 23:17:42 +02:00
9 changed files with 72 additions and 148 deletions
@@ -69,5 +69,3 @@ Logarithmic in the size of the container, O(log(`size()`)).
## Version history
- Since version 2.0.8.
- Fixed in version 3.13.0: for [`ordered_json`](../ordered_json.md), the value could previously only be passed as an
rvalue; it can now also be passed as an lvalue or a `#!cpp const` lvalue, matching the behavior of `json`.
+11 -3
View File
@@ -89,6 +89,9 @@ Strong exception safety: if an exception occurs, the original value stays intact
- Throws [`out_of_range.410`](../../home/exceptions.md#jsonexceptionout_of_range410) if an array index in the passed
JSON pointer `ptr` exceeds the range of `size_type` (e.g., on 32-bit platforms).
For the **const** version, an object key or array index in `ptr` that does not exist is not reported by an
exception, but is undefined behavior (see the notes below). Use [`at`](at.md) for checked access.
## Complexity
1. Constant if `idx` is in the range of the array. Otherwise, linear in `idx - size()`.
@@ -103,9 +106,12 @@ Strong exception safety: if an exception occurs, the original value stays intact
The following cases apply to the **const** overloads; the non-const overloads instead insert the missing element
(see the notes below).
1. If the element at index `idx` does not exist, the behavior is undefined.
1. If the element at index `idx` does not exist, the behavior is undefined and is **guarded by a
[runtime assertion](../../features/assertions.md)**!
2. If the element with key `key` does not exist, the behavior is undefined and is **guarded by a
[runtime assertion](../../features/assertions.md)**!
3. If the JSON pointer `ptr` refers to an object key or an array index that does not exist, the behavior is
undefined and is **guarded by a [runtime assertion](../../features/assertions.md)**!
1. The non-const version may add values: If `idx` is beyond the range of the array (i.e., `idx >= size()`), then the
array is silently filled up with `#!json null` values to make `idx` a valid reference to the last stored element. In
@@ -261,9 +267,11 @@ Strong exception safety: if an exception occurs, the original value stays intact
## Version history
1. Added in version 1.0.0. Fixed in version 3.13.0 to throw `#!cpp std::length_error` instead of emptying the array and
accessing it out of bounds when `idx` equals the maximum value of `size_type`.
accessing it out of bounds when `idx` equals the maximum value of `size_type`. A missing index in the const version
is guarded by a runtime assertion since version 3.13.0.
2. Added in version 1.0.0. Added overloads for `T* key` in version 1.1.0. Removed overloads for `T* key` (replaced by 3)
in version 3.11.0.
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.
4. Added in version 2.0.0.
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.
+31 -7
View File
@@ -16,14 +16,15 @@ before including the `json.hpp` header.
## Function with runtime assertions
### Unchecked object access to a const value
### Unchecked access to a const value
Function [`operator[]`](../api/basic_json/operator%5B%5D.md) implements unchecked access for objects. Whereas a missing
key is added in the case of non-const objects, accessing a const object with a missing key is undefined behavior (think
of a dereferenced null pointer) and yields a runtime assertion.
Function [`operator[]`](../api/basic_json/operator%5B%5D.md) implements unchecked access for arrays and objects. Whereas
a missing element is added in the case of non-const values, accessing a const value with a missing object key or an
invalid array index is undefined behavior (think of a dereferenced null pointer) and yields a runtime assertion. This
also applies to a [JSON pointer](json_pointer.md) that refers to a missing key or an invalid index.
If you are not sure whether an element in an object exists, use checked access with the
[`at` function](../api/basic_json/at.md) or call the [`contains` function](../api/basic_json/contains.md) before.
If you are not sure whether an element exists, use checked access with the [`at` function](../api/basic_json/at.md)
or call the [`contains` function](../api/basic_json/contains.md) before.
See also the documentation on [element access](element_access/index.md).
@@ -46,7 +47,30 @@ See also the documentation on [element access](element_access/index.md).
Output:
```
Assertion failed: (m_value.object->find(key) != m_value.object->end()), function operator[], file json.hpp, line 2144.
Assertion failed: (it != m_data.m_value.object->end()), function operator[], file json.hpp, line 28795.
```
??? example "Example 2: Invalid array index in a JSON pointer"
The following code will trigger an assertion at runtime:
```cpp
#include <nlohmann/json.hpp>
using json = nlohmann::json;
using namespace nlohmann::literals;
int main()
{
const json j = {{"array", {1, 2, 3}}};
auto v = j["/array/5"_json_pointer];
}
```
Output:
```
Assertion failed: (idx < m_data.m_value.array->size()), function operator[], file json.hpp, line 28758.
```
### Constructing from an uninitialized iterator range
+8 -2
View File
@@ -536,6 +536,10 @@ class json_pointer
@return const reference to the JSON value pointed to by the JSON
pointer
@pre Every object key and array index the pointer refers to exists.
Like the const operator[] for keys and indices, a missing one is
undefined behavior, guarded by a runtime assertion.
@throw parse_error.106 if an array index begins with '0'
@throw parse_error.109 if an array index was not a number
@throw out_of_range.402 if the array index '-' is used
@@ -550,7 +554,8 @@ class json_pointer
{
case detail::value_t::object:
{
// use unchecked object access
// use unchecked object access; the const operator[]
// asserts that the key exists
ptr = &ptr->operator[](reference_token);
break;
}
@@ -563,7 +568,8 @@ class json_pointer
JSON_THROW(detail::out_of_range::create(402, detail::concat("array index '-' (", std::to_string(ptr->m_data.m_value.array->size()), ") is out of range"), ptr));
}
// use unchecked array access
// use unchecked array access; the const operator[]
// asserts that the index exists
ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token));
break;
}
+1
View File
@@ -2874,6 +2874,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
// const operator[] only works for arrays
if (JSON_HEDLEY_LIKELY(is_array()))
{
JSON_ASSERT(idx < m_data.m_value.array->size());
return m_data.m_value.array->operator[](idx);
}
+6 -9
View File
@@ -72,9 +72,7 @@ template <class Key, class T, class IgnoredLess = std::less<Key>,
return *this;
}
template<class V, detail::enable_if_t<
detail::is_constructible<T, V>::value, int> = 0>
std::pair<iterator, bool> emplace(const key_type& key, V && t)
std::pair<iterator, bool> emplace(const key_type& key, T&& t)
{
for (auto it = this->begin(); it != this->end(); ++it)
{
@@ -83,14 +81,13 @@ template <class Key, class T, class IgnoredLess = std::less<Key>,
return {it, false};
}
}
append(key, std::forward<V>(t));
append(key, std::forward<T>(t));
return {std::prev(this->end()), true};
}
template<class KeyType, class V, detail::enable_if_t<
detail::conjunction<detail::is_usable_as_key_type<key_compare, key_type, KeyType>,
detail::is_constructible<T, V>>::value, int> = 0>
std::pair<iterator, bool> emplace(KeyType && key, V && t)
template<class KeyType, detail::enable_if_t<
detail::is_usable_as_key_type<key_compare, key_type, KeyType>::value, int> = 0>
std::pair<iterator, bool> emplace(KeyType && key, T && t)
{
for (auto it = this->begin(); it != this->end(); ++it)
{
@@ -99,7 +96,7 @@ template <class Key, class T, class IgnoredLess = std::less<Key>,
return {it, false};
}
}
append(std::forward<KeyType>(key), std::forward<V>(t));
append(std::forward<KeyType>(key), std::forward<T>(t));
return {std::prev(this->end()), true};
}
+15 -11
View File
@@ -20148,6 +20148,10 @@ class json_pointer
@return const reference to the JSON value pointed to by the JSON
pointer
@pre Every object key and array index the pointer refers to exists.
Like the const operator[] for keys and indices, a missing one is
undefined behavior, guarded by a runtime assertion.
@throw parse_error.106 if an array index begins with '0'
@throw parse_error.109 if an array index was not a number
@throw out_of_range.402 if the array index '-' is used
@@ -20162,7 +20166,8 @@ class json_pointer
{
case detail::value_t::object:
{
// use unchecked object access
// use unchecked object access; the const operator[]
// asserts that the key exists
ptr = &ptr->operator[](reference_token);
break;
}
@@ -20175,7 +20180,8 @@ class json_pointer
JSON_THROW(detail::out_of_range::create(402, detail::concat("array index '-' (", std::to_string(ptr->m_data.m_value.array->size()), ") is out of range"), ptr));
}
// use unchecked array access
// use unchecked array access; the const operator[]
// asserts that the index exists
ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token));
break;
}
@@ -26622,9 +26628,7 @@ template <class Key, class T, class IgnoredLess = std::less<Key>,
return *this;
}
template<class V, detail::enable_if_t<
detail::is_constructible<T, V>::value, int> = 0>
std::pair<iterator, bool> emplace(const key_type& key, V && t)
std::pair<iterator, bool> emplace(const key_type& key, T&& t)
{
for (auto it = this->begin(); it != this->end(); ++it)
{
@@ -26633,14 +26637,13 @@ template <class Key, class T, class IgnoredLess = std::less<Key>,
return {it, false};
}
}
append(key, std::forward<V>(t));
append(key, std::forward<T>(t));
return {std::prev(this->end()), true};
}
template<class KeyType, class V, detail::enable_if_t<
detail::conjunction<detail::is_usable_as_key_type<key_compare, key_type, KeyType>,
detail::is_constructible<T, V>>::value, int> = 0>
std::pair<iterator, bool> emplace(KeyType && key, V && t)
template<class KeyType, detail::enable_if_t<
detail::is_usable_as_key_type<key_compare, key_type, KeyType>::value, int> = 0>
std::pair<iterator, bool> emplace(KeyType && key, T && t)
{
for (auto it = this->begin(); it != this->end(); ++it)
{
@@ -26649,7 +26652,7 @@ template <class Key, class T, class IgnoredLess = std::less<Key>,
return {it, false};
}
}
append(std::forward<KeyType>(key), std::forward<V>(t));
append(std::forward<KeyType>(key), std::forward<T>(t));
return {std::prev(this->end()), true};
}
@@ -29804,6 +29807,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
// const operator[] only works for arrays
if (JSON_HEDLEY_LIKELY(is_array()))
{
JSON_ASSERT(idx < m_data.m_value.array->size());
return m_data.m_value.array->operator[](idx);
}
-41
View File
@@ -196,44 +196,3 @@ TEST_CASE("regression test - diff() must account for ordered_json member order")
CHECK(a.patch(p) == b);
}
}
TEST_CASE("regression test for issue #5673 - ordered_json::emplace with a non-rvalue value")
{
SECTION("lvalue value")
{
ordered_json oj = ordered_json::object();
ordered_json value = 1;
auto res = oj.emplace("a", value);
CHECK(res.second == true);
CHECK(oj.dump() == "{\"a\":1}");
}
SECTION("const lvalue value")
{
ordered_json oj = ordered_json::object();
const ordered_json value = 1;
auto res = oj.emplace("a", value);
CHECK(res.second == true);
CHECK(oj.dump() == "{\"a\":1}");
}
SECTION("rvalue value")
{
ordered_json oj = ordered_json::object();
auto res = oj.emplace("a", ordered_json(1));
CHECK(res.second == true);
CHECK(oj.dump() == "{\"a\":1}");
}
SECTION("existing key is not overwritten (std::map-compatible semantics)")
{
ordered_json oj = ordered_json::object();
ordered_json value = 1;
oj.emplace("a", value);
ordered_json other_value = 2;
auto res = oj.emplace("a", other_value);
CHECK(res.second == false);
CHECK(oj.dump() == "{\"a\":1}");
}
}
-73
View File
@@ -403,79 +403,6 @@ TEST_CASE("ordered_map")
CHECK(om.size() == 4);
}
}
SECTION("emplace")
{
// regression test for issue #5673: the mapped-value parameter must
// accept lvalues and const lvalues, not just rvalues
ordered_map<std::string, std::string> om;
om["eins"] = "one";
om["zwei"] = "two";
om["drei"] = "three";
SECTION("with T&& (rvalue)")
{
auto res1 = om.emplace("eins", std::string("1"));
CHECK(res1.first == om.begin());
CHECK(res1.second == false);
CHECK(om.size() == 3);
CHECK(om.at("eins") == "one"); // existing key is not overwritten
auto res4 = om.emplace("vier", std::string("four"));
CHECK(res4.first == om.begin() + 3);
CHECK(res4.second == true);
CHECK(om.size() == 4);
CHECK(om.at("vier") == "four");
}
SECTION("with T& (lvalue)")
{
std::string one = "1";
std::string four = "four";
auto res1 = om.emplace("eins", one);
CHECK(res1.first == om.begin());
CHECK(res1.second == false);
CHECK(om.size() == 3);
CHECK(om.at("eins") == "one"); // existing key is not overwritten
auto res4 = om.emplace("vier", four);
CHECK(res4.first == om.begin() + 3);
CHECK(res4.second == true);
CHECK(om.size() == 4);
CHECK(om.at("vier") == "four");
CHECK(four == "four"); // source was copied, not moved from
}
SECTION("with const T&")
{
const std::string one = "1";
const std::string four = "four";
auto res1 = om.emplace("eins", one);
CHECK(res1.first == om.begin());
CHECK(res1.second == false);
CHECK(om.size() == 3);
auto res4 = om.emplace("vier", four);
CHECK(res4.first == om.begin() + 3);
CHECK(res4.second == true);
CHECK(om.size() == 4);
CHECK(om.at("vier") == "four");
}
SECTION("with key of key_type (non-template overload)")
{
const std::string key_vier{"vier"};
std::string four = "four";
auto res4 = om.emplace(key_vier, four);
CHECK(res4.first == om.begin() + 3);
CHECK(res4.second == true);
CHECK(om.size() == 4);
CHECK(om.at("vier") == "four");
}
}
}
TEST_CASE("ordered_map growth")