From fdcc569eee979cb8237c5a2c61e290c527b2d487 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 30 Sep 2026 20:13:31 +0200 Subject: [PATCH] Use only documented StringType members in json_pointer (#5692) contains(const json_pointer&) and operator/=(std::size_t) (and hence operator/(std::size_t)) used string_t operations that the StringType template parameter documentation explicitly does not require: comparing string_t with a const char* literal, c_str(), and constructibility from std::string. This made both functions fail to compile for a conforming custom StringType, even though the documentation's own reference StringType satisfies the requirements. Fix contains() to compare individual chars ('0'..'9') instead of comparing string_t with const char* literals, and to call data() (documented to be null-terminated) instead of c_str(). Fix operator/=(std::size_t) to build the array-index token via the existing detail::to_string helper (ADL int_to_string() or assignment from std::to_string()) instead of via std::to_string() directly, matching how diff(), items(), and std::hash already convert a std::size_t to a StringType. Add regression tests to tests/src/unit-alt-string.cpp: contains() for present/missing keys and indices, "-", a leading zero, and a non-numeric token on an array, plus json_pointer::operator/(std::size_t). Fixes #5666. Signed-off-by: Niels Lohmann --- .../features/types/template_parameters.md | 1 + include/nlohmann/detail/json_pointer.hpp | 7 ++-- single_include/nlohmann/json.hpp | 8 +++-- tests/src/unit-alt-string.cpp | 35 +++++++++++++++++++ 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/docs/mkdocs/docs/features/types/template_parameters.md b/docs/mkdocs/docs/features/types/template_parameters.md index 8ed23e156..5b528fd79 100644 --- a/docs/mkdocs/docs/features/types/template_parameters.md +++ b/docs/mkdocs/docs/features/types/template_parameters.md @@ -389,6 +389,7 @@ using array_t = ArrayType>; | Functionality | Additional requirement | |-----------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [`diff`](../../api/basic_json/diff.md), [`items`](../../api/basic_json/items.md), [`std::hash`](../../api/basic_json/std_hash.md) | conversion of a `#!cpp std::size_t` to `StringType`: either assignability from the result of `#!cpp std::to_string`, or an ADL overload `#!cpp void int_to_string(StringType&, std::size_t)` | +| [`operator/(std::size_t)`](../../api/json_pointer/operator_slash.md) | the same conversion of a `#!cpp std::size_t` to `StringType` as `diff`, `items`, and `std::hash` above | | [`std::hash`](../../api/basic_json/std_hash.md) | additionally a specialization of `#!cpp std::hash` | | [`to_bson`](../../api/basic_json/to_bson.md) | `find(value_type)` and `npos` | | [`parse`](../../api/basic_json/parse.md) from a `string_t` | the input adapters must accept it; otherwise pass a character range | diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index 0b9f9651a..78074988e 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -26,6 +26,7 @@ #include #include #include +#include #include NLOHMANN_JSON_NAMESPACE_BEGIN @@ -116,7 +117,7 @@ class json_pointer /// @sa https://json.nlohmann.me/api/json_pointer/operator_slasheq/ json_pointer& operator/=(std::size_t array_idx) { - return *this /= std::to_string(array_idx); + return *this /= detail::to_string(array_idx); } /// @brief create a new JSON pointer by appending the right JSON pointer at the end of the left JSON pointer @@ -752,7 +753,7 @@ class json_pointer // would throw out_of_range.404 -- contains() must not throw (see #5395) return false; } - if (JSON_HEDLEY_UNLIKELY(reference_token.size() == 1 && !("0" <= reference_token && reference_token <= "9"))) + if (JSON_HEDLEY_UNLIKELY(reference_token.size() == 1 && !('0' <= reference_token[0] && reference_token[0] <= '9'))) { // invalid char return false; @@ -780,7 +781,7 @@ class json_pointer // not throw (see #5395), so such a reference token is treated as "not found" errno = 0; // strtoull() does not reset errno on success char* p_end = nullptr; // NOLINT(misc-const-correctness) - const unsigned long long magnitude = std::strtoull(reference_token.c_str(), &p_end, 10); // NOLINT(runtime/int) + const unsigned long long magnitude = std::strtoull(reference_token.data(), &p_end, 10); // NOLINT(runtime/int) if (JSON_HEDLEY_UNLIKELY(errno == ERANGE // the value exceeds ULLONG_MAX || magnitude >= static_cast((std::numeric_limits::max)()))) // NOLINT(runtime/int) { diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index cf94c53a9..88c29a4f1 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -19636,6 +19636,8 @@ NLOHMANN_JSON_NAMESPACE_END // #include +// #include + // #include @@ -19727,7 +19729,7 @@ class json_pointer /// @sa https://json.nlohmann.me/api/json_pointer/operator_slasheq/ json_pointer& operator/=(std::size_t array_idx) { - return *this /= std::to_string(array_idx); + return *this /= detail::to_string(array_idx); } /// @brief create a new JSON pointer by appending the right JSON pointer at the end of the left JSON pointer @@ -20363,7 +20365,7 @@ class json_pointer // would throw out_of_range.404 -- contains() must not throw (see #5395) return false; } - if (JSON_HEDLEY_UNLIKELY(reference_token.size() == 1 && !("0" <= reference_token && reference_token <= "9"))) + if (JSON_HEDLEY_UNLIKELY(reference_token.size() == 1 && !('0' <= reference_token[0] && reference_token[0] <= '9'))) { // invalid char return false; @@ -20391,7 +20393,7 @@ class json_pointer // not throw (see #5395), so such a reference token is treated as "not found" errno = 0; // strtoull() does not reset errno on success char* p_end = nullptr; // NOLINT(misc-const-correctness) - const unsigned long long magnitude = std::strtoull(reference_token.c_str(), &p_end, 10); // NOLINT(runtime/int) + const unsigned long long magnitude = std::strtoull(reference_token.data(), &p_end, 10); // NOLINT(runtime/int) if (JSON_HEDLEY_UNLIKELY(errno == ERANGE // the value exceeds ULLONG_MAX || magnitude >= static_cast((std::numeric_limits::max)()))) // NOLINT(runtime/int) { diff --git a/tests/src/unit-alt-string.cpp b/tests/src/unit-alt-string.cpp index f95d2213c..cac2d183d 100644 --- a/tests/src/unit-alt-string.cpp +++ b/tests/src/unit-alt-string.cpp @@ -352,6 +352,41 @@ TEST_CASE("alternative string type") CHECK(j2.flatten().unflatten() == j2); } + SECTION("contains(json_pointer)") + { + // contains(json_pointer) must compile and work with a string_t that has + // no c_str() and no comparison with const char* (see #5666) + auto j = alt_json::parse(R"({"foo": ["bar", "baz"]})"); + + // present: object key and array indices + CHECK(j.contains(alt_json::json_pointer("/foo"))); + CHECK(j.contains(alt_json::json_pointer("/foo/0"))); + CHECK(j.contains(alt_json::json_pointer("/foo/1"))); + + // missing: absent object key and out-of-range array index + CHECK_FALSE(j.contains(alt_json::json_pointer("/bar"))); + CHECK_FALSE(j.contains(alt_json::json_pointer("/foo/2"))); + + // "-" always fails the range check + CHECK_FALSE(j.contains(alt_json::json_pointer("/foo/-"))); + + // an array index must not have a leading zero + CHECK_FALSE(j.contains(alt_json::json_pointer("/foo/01"))); + + // a reference token that is not a number + CHECK_FALSE(j.contains(alt_json::json_pointer("/foo/bar"))); + } + + SECTION("operator/(std::size_t)") + { + // json_pointer::operator/=(std::size_t) must compile without string_t + // being constructible from std::string (see #5666) + auto j = alt_json::parse(R"({"foo": ["bar", "baz"]})"); + + CHECK(j.at(alt_json::json_pointer("/foo") / std::size_t(0)) == j["foo"][0]); + CHECK(j.at(alt_json::json_pointer("/foo") / std::size_t(1)) == j["foo"][1]); + } + SECTION("patch") { alt_json const patch1 = alt_json::parse(R"([{ "op": "add", "path": "/a/b", "value": [ "foo", "bar" ] }])");