Compare commits

..
Author SHA1 Message Date
Niels Lohmann af8149c483 Use only documented StringType members in json_pointer
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<StringType> 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 <mail@nlohmann.me>
2026-09-29 23:36:58 +02:00
7 changed files with 53 additions and 116 deletions
+4 -12
View File
@@ -6,9 +6,7 @@ void swap(reference other) noexcept (
std::is_nothrow_move_constructible<value_t>::value &&
std::is_nothrow_move_assignable<value_t>::value &&
std::is_nothrow_move_constructible<json_value>::value &&
std::is_nothrow_move_assignable<json_value>::value &&
std::is_nothrow_move_constructible<json_base_class_t>::value &&
std::is_nothrow_move_assignable<json_base_class_t>::value
std::is_nothrow_move_assignable<json_value>::value
);
// (2)
@@ -16,9 +14,7 @@ friend void swap(reference left, reference right) noexcept (
std::is_nothrow_move_constructible<value_t>::value &&
std::is_nothrow_move_assignable<value_t>::value &&
std::is_nothrow_move_constructible<json_value>::value &&
std::is_nothrow_move_assignable<json_value>::value &&
std::is_nothrow_move_constructible<json_base_class_t>::value &&
std::is_nothrow_move_assignable<json_base_class_t>::value
std::is_nothrow_move_assignable<json_value>::value
);
// (3)
@@ -41,15 +37,11 @@ void swap(typename binary_t::container_type& other);
individual elements. All iterators and references remain valid. The past-the-end iterator is invalidated. If macro
[`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) is defined to `#!cpp 1`, the
[`start_pos()`](start_pos.md)/[`end_pos()`](end_pos.md) diagnostic positions are exchanged along with the value.
The [`json_base_class_t`](json_base_class_t.md) subobject is exchanged along with the value as well, the same way it
is copied or moved by the copy/move constructors and assignment operators.
2. Exchanges the contents of the JSON value from `left` with those of `right`. Does not invoke any move, copy, or swap
operations on individual elements. All iterators and references remain valid. The past-the-end iterator is
invalidated. Implemented as a friend function callable via ADL. If macro
[`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) is defined to `#!cpp 1`, the
[`start_pos()`](start_pos.md)/[`end_pos()`](end_pos.md) diagnostic positions are exchanged along with the value.
The [`json_base_class_t`](json_base_class_t.md) subobject is exchanged along with the value as well, the same way it
is copied or moved by the copy/move constructors and assignment operators.
3. Exchanges the contents of a JSON array with those of `other`. Does not invoke any move, copy, or swap operations on
individual elements. All iterators and references remain valid. The past-the-end iterator is invalidated.
4. Exchanges the contents of a JSON object with those of `other`. Does not invoke any move, copy, or swap operations on
@@ -172,8 +164,8 @@ Constant.
## Version history
1. Since version 1.0.0. Exchanges the `json_base_class_t` subobject along with the value since version 3.13.0.
2. Since version 1.0.0. Exchanges the `json_base_class_t` subobject along with the value since version 3.13.0.
1. Since version 1.0.0.
2. Since version 1.0.0.
3. Since version 1.0.0.
4. Since version 1.0.0.
5. Since version 1.0.0.
@@ -389,6 +389,7 @@ using array_t = ArrayType<basic_json, AllocatorType<basic_json>>;
| 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<basic_json>`](../../api/basic_json/std_hash.md) | additionally a specialization of `#!cpp std::hash<StringType>` |
| [`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 |
+4 -3
View File
@@ -26,6 +26,7 @@
#include <nlohmann/detail/macro_scope.hpp>
#include <nlohmann/detail/string_concat.hpp>
#include <nlohmann/detail/string_escape.hpp>
#include <nlohmann/detail/string_utils.hpp>
#include <nlohmann/detail/value_t.hpp>
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<string_t>(array_idx);
}
/// @brief create a new JSON pointer by appending the right JSON pointer at the end of the left JSON pointer
@@ -746,7 +747,7 @@ class json_pointer
// "-" always fails the range check
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;
@@ -774,7 +775,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<unsigned long long>((std::numeric_limits<typename BasicJsonType::size_type>::max)()))) // NOLINT(runtime/int)
{
+2 -13
View File
@@ -4335,21 +4335,12 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
std::is_nothrow_move_constructible<value_t>::value&&
std::is_nothrow_move_assignable<value_t>::value&&
std::is_nothrow_move_constructible<json_value>::value&& // NOLINT(cppcoreguidelines-noexcept-swap,performance-noexcept-swap)
std::is_nothrow_move_assignable<json_value>::value&&
std::is_nothrow_move_constructible<json_base_class_t>::value&&
std::is_nothrow_move_assignable<json_base_class_t>::value
std::is_nothrow_move_assignable<json_value>::value
)
{
std::swap(m_data.m_type, other.m_data.m_type);
std::swap(m_data.m_value, other.m_data.m_value);
// the custom base class travels with the value when it is copied or
// moved, so it is exchanged along with it
{
using std::swap;
swap(static_cast<json_base_class_t&>(*this), static_cast<json_base_class_t&>(other));
}
#if JSON_DIAGNOSTIC_POSITIONS
std::swap(start_position, other.start_position);
std::swap(end_position, other.end_position);
@@ -4366,9 +4357,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
std::is_nothrow_move_constructible<value_t>::value&&
std::is_nothrow_move_assignable<value_t>::value&&
std::is_nothrow_move_constructible<json_value>::value&& // NOLINT(cppcoreguidelines-noexcept-swap,performance-noexcept-swap)
std::is_nothrow_move_assignable<json_value>::value&&
std::is_nothrow_move_constructible<json_base_class_t>::value&&
std::is_nothrow_move_assignable<json_base_class_t>::value
std::is_nothrow_move_assignable<json_value>::value
)
{
left.swap(right);
+7 -16
View File
@@ -18867,6 +18867,8 @@ NLOHMANN_JSON_NAMESPACE_END
// #include <nlohmann/detail/string_escape.hpp>
// #include <nlohmann/detail/string_utils.hpp>
// #include <nlohmann/detail/value_t.hpp>
@@ -18958,7 +18960,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<string_t>(array_idx);
}
/// @brief create a new JSON pointer by appending the right JSON pointer at the end of the left JSON pointer
@@ -19588,7 +19590,7 @@ class json_pointer
// "-" always fails the range check
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;
@@ -19616,7 +19618,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<unsigned long long>((std::numeric_limits<typename BasicJsonType::size_type>::max)()))) // NOLINT(runtime/int)
{
@@ -30416,21 +30418,12 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
std::is_nothrow_move_constructible<value_t>::value&&
std::is_nothrow_move_assignable<value_t>::value&&
std::is_nothrow_move_constructible<json_value>::value&& // NOLINT(cppcoreguidelines-noexcept-swap,performance-noexcept-swap)
std::is_nothrow_move_assignable<json_value>::value&&
std::is_nothrow_move_constructible<json_base_class_t>::value&&
std::is_nothrow_move_assignable<json_base_class_t>::value
std::is_nothrow_move_assignable<json_value>::value
)
{
std::swap(m_data.m_type, other.m_data.m_type);
std::swap(m_data.m_value, other.m_data.m_value);
// the custom base class travels with the value when it is copied or
// moved, so it is exchanged along with it
{
using std::swap;
swap(static_cast<json_base_class_t&>(*this), static_cast<json_base_class_t&>(other));
}
#if JSON_DIAGNOSTIC_POSITIONS
std::swap(start_position, other.start_position);
std::swap(end_position, other.end_position);
@@ -30447,9 +30440,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
std::is_nothrow_move_constructible<value_t>::value&&
std::is_nothrow_move_assignable<value_t>::value&&
std::is_nothrow_move_constructible<json_value>::value&& // NOLINT(cppcoreguidelines-noexcept-swap,performance-noexcept-swap)
std::is_nothrow_move_assignable<json_value>::value&&
std::is_nothrow_move_constructible<json_base_class_t>::value&&
std::is_nothrow_move_assignable<json_base_class_t>::value
std::is_nothrow_move_assignable<json_value>::value
)
{
left.swap(right);
+35
View File
@@ -343,6 +343,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" ] }])");
-72
View File
@@ -6,11 +6,9 @@
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include <algorithm>
#include <set>
#include <sstream>
#include <string>
#include <vector>
#include "doctest_compatibility.h"
@@ -182,76 +180,6 @@ TEST_CASE("JSON Node Metadata")
CHECK(val.metadata().at(1) == 2);
}
}
SECTION("member swap")
{
using json = json_with_metadata<int>;
json a = 1;
a.metadata() = 100;
json b = 2;
b.metadata() = 200;
a.swap(b);
CHECK(a.get<int>() == 2);
CHECK(b.get<int>() == 1);
CHECK(a.metadata() == 200);
CHECK(b.metadata() == 100);
}
SECTION("nonmember swap")
{
using json = json_with_metadata<int>;
json a = 1;
a.metadata() = 100;
json b = 2;
b.metadata() = 200;
using std::swap;
swap(a, b);
CHECK(a.get<int>() == 2);
CHECK(b.get<int>() == 1);
CHECK(a.metadata() == 200);
CHECK(b.metadata() == 100);
}
SECTION("std::swap")
{
using json = json_with_metadata<int>;
json a = 1;
a.metadata() = 100;
json b = 2;
b.metadata() = 200;
std::swap(a, b);
CHECK(a.get<int>() == 2);
CHECK(b.get<int>() == 1);
CHECK(a.metadata() == 200);
CHECK(b.metadata() == 100);
}
SECTION("std::sort keeps metadata attached to its value")
{
// std::sort mixes swap() with moves; each value's metadata must
// travel with it, just as it does for copy, move, and assignment
using json = json_with_metadata<int>;
std::vector<json> values;
for (int v :
{
5, 3, 9, 1, 7, 2, 8, 4, 6, 0, 15, 13, 19, 11, 17, 12, 18, 14, 16, 10,
25, 23, 29, 21, 27, 22, 28, 24, 26, 20, 35, 33
})
{
json value = v;
value.metadata() = v;
values.push_back(value);
}
std::sort(values.begin(), values.end());
for (const auto& value : values)
{
CHECK(value.metadata() == value.get<int>());
}
}
}
// Test extending nlohmann::json by using a custom base class.