From 6ae17630a4fab15f5b456c08e3cf27dbaf1eef82 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 30 Sep 2026 20:07:45 +0200 Subject: [PATCH] Reject integral keys for contains(), find(), and count() at compile time (#5705) j.contains(0), j.find(0), and j.count(0) used to compile: the literal 0 is a null pointer constant, so it converts to a null const char*, and the overloads taking const typename object_t::key_type& accepted it by constructing a std::string from that null pointer, which is undefined behavior (a crash with both libc++ and libstdc++). value(0, default_value) had the same problem in C++11, where the object comparator is not transparent. Add deleted overloads for integral arguments to contains(), find() (const and non-const), count(), and value() so that these calls are compile errors in every supported language mode instead of crashing. Calls with string, string_view, json_pointer, and size-typed element access (at(), operator[](), erase()) are unaffected. Fixes #5657. Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/contains.md | 6 ++ docs/mkdocs/docs/api/basic_json/count.md | 8 +- docs/mkdocs/docs/api/basic_json/find.md | 8 +- docs/mkdocs/docs/api/basic_json/value.md | 12 ++- include/nlohmann/json.hpp | 14 ++++ single_include/nlohmann/json.hpp | 14 ++++ tests/src/unit-element_access2.cpp | 81 +++++++++++++++++++++ 7 files changed, 140 insertions(+), 3 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/contains.md b/docs/mkdocs/docs/api/basic_json/contains.md index 7f865f83c..84b3c443c 100644 --- a/docs/mkdocs/docs/api/basic_json/contains.md +++ b/docs/mkdocs/docs/api/basic_json/contains.md @@ -58,6 +58,10 @@ Logarithmic in the size of the JSON object. - This method always returns `#!cpp false` when executed on a JSON type that is not an object. - This method can be executed on any JSON value type. +- Calling this function with an integer argument (for example, `#!cpp contains(0)`) does not compile: such an argument + would otherwise implicitly convert to a null `#!cpp const char*` and, from there, cause undefined behavior when + constructing a `#!cpp std::string` for the object key. To check for an array element instead, use [`at`](at.md), + [`operator[]`](operator%5B%5D.md), or compare against [`size`](size.md). !!! info "Postconditions" @@ -117,3 +121,5 @@ Logarithmic in the size of the JSON object. 1. Added in version 3.11.0. 2. Added in version 3.6.0. Extended template `KeyType` to support comparable types in version 3.11.0. 3. Added in version 3.7.0. +4. Deleted overloads for integral key types added in version 3.13.0 to reject such calls at compile time instead of + causing undefined behavior at runtime. diff --git a/docs/mkdocs/docs/api/basic_json/count.md b/docs/mkdocs/docs/api/basic_json/count.md index ce51addbd..bffc46534 100644 --- a/docs/mkdocs/docs/api/basic_json/count.md +++ b/docs/mkdocs/docs/api/basic_json/count.md @@ -40,7 +40,11 @@ Logarithmic in the size of the JSON object. ## Notes -This method always returns `0` when executed on a JSON type that is not an object. +- This method always returns `0` when executed on a JSON type that is not an object. +- Calling this function with an integer argument (for example, `#!cpp count(0)`) does not compile: such an argument + would otherwise implicitly convert to a null `#!cpp const char*` and, from there, cause undefined behavior when + constructing a `#!cpp std::string` for the object key. To check for an array element instead, use [`at`](at.md), + [`operator[]`](operator%5B%5D.md), or compare against [`size`](size.md). ## Examples @@ -81,3 +85,5 @@ This method always returns `0` when executed on a JSON type that is not an objec 1. Added in version 3.11.0. 2. Added in version 1.0.0. Changed parameter `key` type to `KeyType&&` in version 3.11.0. +3. Deleted overload for integral key types added in version 3.13.0 to reject such calls at compile time instead of + causing undefined behavior at runtime. diff --git a/docs/mkdocs/docs/api/basic_json/find.md b/docs/mkdocs/docs/api/basic_json/find.md index 35ff9dcb2..bc746ee2f 100644 --- a/docs/mkdocs/docs/api/basic_json/find.md +++ b/docs/mkdocs/docs/api/basic_json/find.md @@ -44,7 +44,11 @@ Logarithmic in the size of the JSON object. ## Notes -This method always returns `end()` when executed on a JSON type that is not an object. +- This method always returns `end()` when executed on a JSON type that is not an object. +- Calling this function with an integer argument (for example, `#!cpp find(0)`) does not compile: such an argument + would otherwise implicitly convert to a null `#!cpp const char*` and, from there, cause undefined behavior when + constructing a `#!cpp std::string` for the object key. To access an array element instead, use [`at`](at.md), + [`operator[]`](operator%5B%5D.md), or compare against [`size`](size.md). ## Examples @@ -85,3 +89,5 @@ This method always returns `end()` when executed on a JSON type that is not an o 1. Added in version 3.11.0. 2. Added in version 1.0.0. Changed to support comparable types in version 3.11.0. +3. Deleted overloads for integral key types added in version 3.13.0 to reject such calls at compile time instead of + causing undefined behavior at runtime. diff --git a/docs/mkdocs/docs/api/basic_json/value.md b/docs/mkdocs/docs/api/basic_json/value.md index 2e9b85a2b..c0e4b9949 100644 --- a/docs/mkdocs/docs/api/basic_json/value.md +++ b/docs/mkdocs/docs/api/basic_json/value.md @@ -52,6 +52,14 @@ This is equivalent to Python's `dict.get(key, default)`. - Unlike [`operator[]`](operator[].md), this function does not implicitly add an element to the position defined by `key`/`ptr` key. This function is furthermore also applicable to const objects. +!!! note "Integer keys" + + Calling this function with an integer `key` argument (for example, `#!cpp value(0, 1)`) does not compile in + C++11, where `object_comparator_t` is not transparent: such an argument would otherwise implicitly convert to a + null `#!cpp const char*` and, from there, cause undefined behavior when constructing a `#!cpp std::string` for the + object key. To access an array element with a default value, use [`at`](at.md) together with a `#!cpp try`/`#!cpp + catch` block, or compare against [`size`](size.md) instead. + ## Template parameters `KeyType` @@ -184,7 +192,9 @@ changes to any JSON value. ## Version history -1. Added in version 1.0.0. Changed parameter `default_value` type from `const ValueType&` to `ValueType&&` in version 3.11.0. +1. Added in version 1.0.0. Changed parameter `default_value` type from `const ValueType&` to `ValueType&&` in version + 3.11.0. Deleted overload for integral key types added in version 3.13.0 to reject such calls at compile time + instead of causing undefined behavior at runtime. 2. Added in version 3.11.0. Made `ValueType` the first template parameter in version 3.11.2. 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 diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 13274f902..f394af567 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -2980,6 +2980,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec string_t, typename std::decay::type >; public: + // an integer literal 0 would otherwise convert to a null const char* and from there to key_type + template::value, int> = 0> + ValueType value(T, ValueType&&) const = delete; + /// @brief access specified object element with default value /// @sa https://json.nlohmann.me/api/basic_json/value/ template < class ValueType, detail::enable_if_t < @@ -3414,6 +3418,16 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @name lookup /// @{ + // an integer literal 0 would otherwise convert to a null const char* and from there to key_type + template::value, int> = 0> + iterator find(T) = delete; + template::value, int> = 0> + const_iterator find(T) const = delete; + template::value, int> = 0> + size_type count(T) const = delete; + template::value, int> = 0> + bool contains(T) const = delete; + /// @brief find an element in a JSON object /// @sa https://json.nlohmann.me/api/basic_json/find/ iterator find(const typename object_t::key_type& key) diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 02065bdf8..8d48e643a 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -29897,6 +29897,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec string_t, typename std::decay::type >; public: + // an integer literal 0 would otherwise convert to a null const char* and from there to key_type + template::value, int> = 0> + ValueType value(T, ValueType&&) const = delete; + /// @brief access specified object element with default value /// @sa https://json.nlohmann.me/api/basic_json/value/ template < class ValueType, detail::enable_if_t < @@ -30331,6 +30335,16 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @name lookup /// @{ + // an integer literal 0 would otherwise convert to a null const char* and from there to key_type + template::value, int> = 0> + iterator find(T) = delete; + template::value, int> = 0> + const_iterator find(T) const = delete; + template::value, int> = 0> + size_type count(T) const = delete; + template::value, int> = 0> + bool contains(T) const = delete; + /// @brief find an element in a JSON object /// @sa https://json.nlohmann.me/api/basic_json/find/ iterator find(const typename object_t::key_type& key) diff --git a/tests/src/unit-element_access2.cpp b/tests/src/unit-element_access2.cpp index 40e8216d5..04974c251 100644 --- a/tests/src/unit-element_access2.cpp +++ b/tests/src/unit-element_access2.cpp @@ -16,6 +16,40 @@ // build test with C++14 // JSON_HAS_CPP_14 +// used to check at compile time (via is_detected) whether a call is well-formed; see +// https://github.com/nlohmann/json/issues/5657 +// +// note: an integer *literal* (rather than a std::declval() of integral type T) is required to +// reproduce the bug, because only a null pointer constant--an integer literal with value zero, not +// merely a runtime value that happens to be zero--implicitly converts to a null const char*; that is +// why can_call_*_with_0 below hard-code the literal 0 instead of taking it as a template argument +template +using can_call_find = decltype(std::declval().find(std::declval())); + +template +using can_call_count = decltype(std::declval().count(std::declval())); + +template +using can_call_contains = decltype(std::declval().contains(std::declval())); + +template +using can_call_value = decltype(std::declval().value(std::declval(), std::declval())); + +template +using can_call_find_with_0 = decltype(std::declval().find(0)); + +template +using can_call_count_with_0 = decltype(std::declval().count(0)); + +template +using can_call_contains_with_0 = decltype(std::declval().contains(0)); + +template +using can_call_contains_with_0L = decltype(std::declval().contains(0L)); + +template +using can_call_value_with_0 = decltype(std::declval().value(0, 1)); + TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_json) // NOLINT(readability-math-missing-parentheses, bugprone-throwing-static-initialization) { SECTION("object") @@ -1493,6 +1527,53 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j } } } + + SECTION("integral keys for object lookup are rejected at compile time") + { + // https://github.com/nlohmann/json/issues/5657: an integer literal like 0 is a null pointer + // constant, which used to convert to a null const char* and from there--via undefined + // behavior in the std::string constructor--to key_type, so contains(0), find(0), and + // count(0) used to compile and then crash instead of failing to compile + using nlohmann::detail::is_detected; + + CHECK_FALSE(is_detected::value); + CHECK_FALSE(is_detected::value); + CHECK_FALSE(is_detected::value); + CHECK_FALSE(is_detected::value); + // value(0, ...) is only affected in C++11, where the comparator is not transparent; with a + // transparent comparator (C++14 and later), int is already rejected for lacking a + // comparison with the key type, independently of this fix + CHECK_FALSE(is_detected::value); + + // another integral literal type must be rejected as well, not just int + CHECK_FALSE(is_detected::value); + + // the valid overloads must remain callable + CHECK(is_detected::value); + CHECK(is_detected::value); + CHECK(is_detected::value); + CHECK(is_detected::value); + CHECK(is_detected::value); + CHECK(is_detected::value); + CHECK(is_detected::value); + +#ifdef JSON_HAS_CPP_17 + CHECK(is_detected::value); + CHECK(is_detected::value); + CHECK(is_detected::value); +#endif + + // the neighboring size_type overloads for array access are unaffected by the new + // integral-key overloads above (at(), operator[](), and erase() take a size_type) + Json arr = {10, 20, 30}; + const Json arr_const = arr; + CHECK(arr.at(0) == 10); + CHECK(arr_const.at(0) == 10); + CHECK(arr[0] == 10); + CHECK(arr_const[0] == 10); + arr.erase(0); + CHECK(arr.size() == 2); + } } #if !defined(JSON_NOEXCEPTION)