Compare commits

..
Author SHA1 Message Date
Niels Lohmann bc9370c291 Merge branch 'develop' into claude/string-view-convertible-keys-5663
Conflicts:
- docs/mkdocs/docs/api/basic_json/count.md: kept the PR's string_view note on overload 2 and develop's new item 3 (deleted integral-key overload)
- docs/mkdocs/docs/api/basic_json/find.md: same as count.md
- docs/mkdocs/docs/api/basic_json/value.md: kept develop's integral-key note on item 1 and the PR's string_view note on item 2

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-30 20:34:35 +02:00
Niels Lohmann 666801a2c6 Fix key types convertible to std::string_view breaking lookups
Since #4958, a key type implicitly convertible to std::string_view was
accepted by is_usable_as_basic_json_key_type without checking that the
object's comparator can actually compare object_t::key_type with that
key type. The key was then forwarded unchanged to the underlying map,
so const operator[], at, find, count, contains, erase and value failed
to compile (a hard error inside <map>) for a key convertible only to
std::string_view, and value() rejected such keys outright. For keys
convertible to both std::string and std::string_view, the KeyType&&
templates now won overload resolution over the object_t::key_type
overloads and then failed the same way, a regression from 3.12.0. Only
the non-const operator[] worked, because it uses emplace(), which
constructs a std::string from the key explicitly. ordered_json was not
affected, since ordered_map checks comparability itself.

Add a trait, is_string_view_convertible_key_type, that recognizes a key
type that is convertible to std::string_view but not directly
comparable with the object's key type, provided std::string_view itself
is comparable with it. at(), operator[], find(), count(), contains(),
erase() and value() now route such keys through a new lookup_key()
helper that converts them to std::string_view before they reach the
object, matching how the object's transparent comparator already
supports std::string_view lookups.

Fixes #5663.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-29 23:31:37 +02:00
11 changed files with 245 additions and 225 deletions
+3 -1
View File
@@ -224,5 +224,7 @@ Strong exception safety: if an exception occurs, the original value stays intact
1. Added in version 1.0.0.
2. Added in version 1.0.0.
3. Added 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 [`operator[]`](operator[].md), [`value`](value.md), [`find`](find.md), and other lookup
functions.
4. Added in version 2.0.0.
+3 -1
View File
@@ -119,7 +119,9 @@ Logarithmic in the size of the JSON object.
## Version history
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.
2. Added in version 3.6.0. Extended template `KeyType` to support comparable types 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), [`at`](at.md), [`value`](value.md), and other lookup functions.
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.
+3 -1
View File
@@ -84,6 +84,8 @@ Logarithmic in the size of the JSON object.
## Version history
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.
2. Added in version 1.0.0. Changed parameter `key` type to `KeyType&&` 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),
[`at`](at.md), [`value`](value.md), and other lookup functions.
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.
+3 -1
View File
@@ -213,5 +213,7 @@ Strong exception safety: if an exception occurs, the original value stays intact
1. Added in version 1.0.0. Added support for binary types in version 3.8.0.
2. Added in version 1.0.0. Added support for binary types in version 3.8.0.
3. Added in version 1.0.0.
4. Added in version 3.11.0.
4. 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), [`at`](at.md), [`value`](value.md), and other lookup
functions.
5. Added in version 1.0.0.
+3 -1
View File
@@ -88,6 +88,8 @@ Logarithmic in the size of the JSON object.
## Version history
1. Added in version 3.11.0.
2. Added in version 1.0.0. Changed to support comparable types in version 3.11.0.
2. Added in version 1.0.0. Changed to support comparable types 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),
[`at`](at.md), [`value`](value.md), and other lookup functions.
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.
+3 -1
View File
@@ -195,7 +195,9 @@ changes to any JSON value.
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.
2. Added in version 3.11.0. Made `ValueType` the first template parameter in version 3.11.2. Fixed in version 3.13.0
to consistently accept `std::string_view`-convertible keys, as already supported by
[`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
`ptr` through an array unexpectedly threw `out_of_range` instead of returning the resolved element (or
`default_value`, as documented).
+25 -34
View File
@@ -189,37 +189,6 @@ struct actual_object_comparator
template<typename BasicJsonType>
using actual_object_comparator_t = typename actual_object_comparator<BasicJsonType>::type;
template<typename T>
using detect_key_comp = decltype(std::declval<const T&>().key_comp());
// whether ObjectType can be constructed from a pair of Iterator together with
// a copy of its own comparator, the way std::map can: it needs a nested
// key_compare, a const key_comp() convertible to it, and a matching
// (Iterator, Iterator, const key_compare&) constructor.
//
// used to preserve a stateful comparator when a copy is built from a range
// past the iterative deep copy's nesting bound (see copy_object_level); an
// object type that does not satisfy this, such as nlohmann::ordered_map
// (which has key_compare for its std::map-like interface, but no key_comp()),
// keeps default-constructing its comparator, just as it always has
template<typename ObjectType, typename Iterator, typename = void>
struct is_comparator_constructible_object_type_impl : std::false_type {};
template<typename ObjectType, typename Iterator>
struct is_comparator_constructible_object_type_impl <
ObjectType, Iterator, enable_if_t<is_detected<detect_key_compare, ObjectType>::value >>
{
using key_compare = typename ObjectType::key_compare;
static constexpr bool value =
is_detected_convertible<key_compare, detect_key_comp, ObjectType>::value &&
std::is_constructible<ObjectType, Iterator, Iterator, const key_compare&>::value;
};
template<typename ObjectType, typename Iterator>
struct is_comparator_constructible_object_type
: is_comparator_constructible_object_type_impl<ObjectType, Iterator> {};
/////////////////
// char_traits //
/////////////////
@@ -779,6 +748,30 @@ using is_usable_as_key_type = typename std::conditional <
std::true_type,
std::false_type >::type;
#ifdef JSON_HAS_CPP_17
// type trait to check if KeyType can only be used as an object key after
// converting it to std::string_view: it is convertible to std::string_view, the
// object's comparator cannot compare it with object_t::key_type directly, but
// can compare a std::string_view. JSON pointers and JSON iterators are ruled out
// first, so that the conversion checks are never instantiated for them (a JSON
// pointer's deprecated conversion to string_t would be named otherwise).
template < typename BasicJsonType, typename KeyTypeCVRef, typename KeyType = uncvref_t<KeyTypeCVRef>,
bool = is_json_pointer<KeyType>::value || is_json_iterator_of<BasicJsonType, KeyType>::value >
struct is_string_view_convertible_key_type : std::false_type {};
template<typename BasicJsonType, typename KeyTypeCVRef, typename KeyType>
struct is_string_view_convertible_key_type<BasicJsonType, KeyTypeCVRef, KeyType, false>
: std::integral_constant < bool,
std::is_convertible<KeyTypeCVRef, std::string_view>::value
&& !is_usable_as_key_type<typename BasicJsonType::object_comparator_t,
typename BasicJsonType::object_t::key_type, KeyTypeCVRef, true, false>::value
&& is_usable_as_key_type<typename BasicJsonType::object_comparator_t,
typename BasicJsonType::object_t::key_type, std::string_view, true, false>::value > {};
#else
template<typename BasicJsonType, typename KeyTypeCVRef>
struct is_string_view_convertible_key_type : std::false_type {};
#endif
// type trait to check if KeyType can be used as an object key
// true if:
// - KeyType is comparable with BasicJsonType::object_t::key_type
@@ -792,9 +785,7 @@ using is_usable_as_basic_json_key_type = typename std::conditional <
typename BasicJsonType::object_t::key_type, KeyTypeCVRef,
RequireTransparentComparator, ExcludeObjectKeyType>::value
&& !is_json_iterator_of<BasicJsonType, KeyType>::value)
#ifdef JSON_HAS_CPP_17
|| std::is_convertible<KeyType, std::string_view>::value
#endif
|| is_string_view_convertible_key_type<BasicJsonType, KeyTypeCVRef>::value
, std::true_type,
std::false_type >::type;
+33 -35
View File
@@ -807,6 +807,24 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
return it;
}
/// @brief the key to look up an object member with: the key itself, or its
/// std::string_view if the object can only be searched with that
template < typename KeyType, detail::enable_if_t <
!detail::is_string_view_convertible_key_type<basic_json_t, KeyType>::value, int > = 0 >
static KeyType && lookup_key(KeyType && key) noexcept
{
return std::forward<KeyType>(key);
}
#ifdef JSON_HAS_CPP_17
template < typename KeyType, detail::enable_if_t <
detail::is_string_view_convertible_key_type<basic_json_t, KeyType>::value, int > = 0 >
static std::string_view lookup_key(KeyType && key)
{
return std::forward<KeyType>(key);
}
#endif
/// @brief erase an element from the object and return the following one
/// Not every map returns an iterator from erase(iterator): some containers
/// (e.g., Abseil's hash maps) return void to avoid computing a successor
@@ -1127,27 +1145,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
}
}
/// @brief create the object type from a range, preserving @a src_object's
/// comparator when the object type supports it
/// Enabled for object types that provide a key_comp() and a matching
/// range-plus-comparator constructor, such as std::map. Other object
/// types, such as nlohmann::ordered_map, fall back to the plain range
/// constructor and default-construct their comparator, just as they
/// always have (@ref detail::is_comparator_constructible_object_type).
template<typename Iterator, detail::enable_if_t<
detail::is_comparator_constructible_object_type<object_t, Iterator>::value, int> = 0>
static object_t* create_object_with_comparator(const object_t& src_object, Iterator first, Iterator last)
{
return create<object_t>(first, last, src_object.key_comp());
}
template<typename Iterator, detail::enable_if_t<
detail::negation<detail::is_comparator_constructible_object_type<object_t, Iterator>>::value, int> = 0>
static object_t* create_object_with_comparator(const object_t& /*src_object*/, Iterator first, Iterator last)
{
return create<object_t>(first, last);
}
/// @brief create the copy of the object @a src in @a dst
/// @note structured values are appended to @a worklist instead
static void copy_object_level(const basic_json& src, basic_json& dst,
@@ -1165,8 +1162,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
scratch.emplace_back(element.first, basic_json());
}
dst.m_data.m_value.object = create_object_with_comparator(src_object,
std::make_move_iterator(scratch.begin()),
dst.m_data.m_value.object = create<object_t>(std::make_move_iterator(scratch.begin()),
std::make_move_iterator(scratch.end()));
// only now that the object exists may dst stop being a null value
dst.m_data.m_type = value_t::object;
@@ -2789,7 +2785,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", type_name()), this));
}
auto it = m_data.m_value.object->find(std::forward<KeyType>(key));
auto it = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
if (it == m_data.m_value.object->end())
{
JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(std::forward<KeyType>(key)), "' not found"), this));
@@ -2827,7 +2823,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", type_name()), this));
}
auto it = m_data.m_value.object->find(std::forward<KeyType>(key));
auto it = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
if (it == m_data.m_value.object->end())
{
JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(std::forward<KeyType>(key)), "' not found"), this));
@@ -2970,7 +2966,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
// operator[] only works for objects
if (JSON_HEDLEY_LIKELY(is_object()))
{
auto result = m_data.m_value.object->emplace(std::forward<KeyType>(key), nullptr);
auto result = m_data.m_value.object->emplace(lookup_key(std::forward<KeyType>(key)), nullptr);
return set_parent(result.first->second);
}
@@ -2986,7 +2982,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
// const operator[] only works for objects
if (JSON_HEDLEY_LIKELY(is_object()))
{
auto it = m_data.m_value.object->find(std::forward<KeyType>(key));
auto it = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
JSON_ASSERT(it != m_data.m_value.object->end());
return it->second;
}
@@ -2996,8 +2992,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
private:
template<typename KeyType>
using is_comparable_with_object_key = detail::is_comparable <
object_comparator_t, const typename object_t::key_type&, KeyType >;
using is_comparable_with_object_key = std::integral_constant < bool,
detail::is_comparable <
object_comparator_t, const typename object_t::key_type&, KeyType >::value
|| detail::is_string_view_convertible_key_type<basic_json_t, KeyType>::value >;
template<typename ValueType>
using value_return_type = std::conditional <
@@ -3384,7 +3382,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this));
}
const auto it = m_data.m_value.object->find(std::forward<KeyType>(key));
const auto it = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
if (it != m_data.m_value.object->end())
{
m_data.m_value.object->erase(it);
@@ -3411,7 +3409,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
detail::is_usable_as_basic_json_key_type<basic_json_t, KeyType>::value, int> = 0>
size_type erase(KeyType && key)
{
return erase_internal(std::forward<KeyType>(key));
return erase_internal(lookup_key(std::forward<KeyType>(key)));
}
/// @brief remove element from a JSON array given an index
@@ -3491,7 +3489,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
if (is_object())
{
result.m_it.object_iterator = m_data.m_value.object->find(std::forward<KeyType>(key));
result.m_it.object_iterator = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
}
return result;
@@ -3507,7 +3505,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
if (is_object())
{
result.m_it.object_iterator = m_data.m_value.object->find(std::forward<KeyType>(key));
result.m_it.object_iterator = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
}
return result;
@@ -3530,7 +3528,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
size_type count(KeyType && key) const
{
// return 0 for all nonobject types
return is_object() ? m_data.m_value.object->count(std::forward<KeyType>(key)) : 0;
return is_object() ? m_data.m_value.object->count(lookup_key(std::forward<KeyType>(key))) : 0;
}
/// @brief check the existence of an element in a JSON object
@@ -3548,7 +3546,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
JSON_HEDLEY_WARN_UNUSED_RESULT
bool contains(KeyType && key) const
{
return is_object() && m_data.m_value.object->find(std::forward<KeyType>(key)) != m_data.m_value.object->end();
return is_object() && m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key))) != m_data.m_value.object->end();
}
/// @brief check the existence of an element in a JSON object given a JSON pointer
+58 -69
View File
@@ -4197,37 +4197,6 @@ struct actual_object_comparator
template<typename BasicJsonType>
using actual_object_comparator_t = typename actual_object_comparator<BasicJsonType>::type;
template<typename T>
using detect_key_comp = decltype(std::declval<const T&>().key_comp());
// whether ObjectType can be constructed from a pair of Iterator together with
// a copy of its own comparator, the way std::map can: it needs a nested
// key_compare, a const key_comp() convertible to it, and a matching
// (Iterator, Iterator, const key_compare&) constructor.
//
// used to preserve a stateful comparator when a copy is built from a range
// past the iterative deep copy's nesting bound (see copy_object_level); an
// object type that does not satisfy this, such as nlohmann::ordered_map
// (which has key_compare for its std::map-like interface, but no key_comp()),
// keeps default-constructing its comparator, just as it always has
template<typename ObjectType, typename Iterator, typename = void>
struct is_comparator_constructible_object_type_impl : std::false_type {};
template<typename ObjectType, typename Iterator>
struct is_comparator_constructible_object_type_impl <
ObjectType, Iterator, enable_if_t<is_detected<detect_key_compare, ObjectType>::value >>
{
using key_compare = typename ObjectType::key_compare;
static constexpr bool value =
is_detected_convertible<key_compare, detect_key_comp, ObjectType>::value &&
std::is_constructible<ObjectType, Iterator, Iterator, const key_compare&>::value;
};
template<typename ObjectType, typename Iterator>
struct is_comparator_constructible_object_type
: is_comparator_constructible_object_type_impl<ObjectType, Iterator> {};
/////////////////
// char_traits //
/////////////////
@@ -4787,6 +4756,30 @@ using is_usable_as_key_type = typename std::conditional <
std::true_type,
std::false_type >::type;
#ifdef JSON_HAS_CPP_17
// type trait to check if KeyType can only be used as an object key after
// converting it to std::string_view: it is convertible to std::string_view, the
// object's comparator cannot compare it with object_t::key_type directly, but
// can compare a std::string_view. JSON pointers and JSON iterators are ruled out
// first, so that the conversion checks are never instantiated for them (a JSON
// pointer's deprecated conversion to string_t would be named otherwise).
template < typename BasicJsonType, typename KeyTypeCVRef, typename KeyType = uncvref_t<KeyTypeCVRef>,
bool = is_json_pointer<KeyType>::value || is_json_iterator_of<BasicJsonType, KeyType>::value >
struct is_string_view_convertible_key_type : std::false_type {};
template<typename BasicJsonType, typename KeyTypeCVRef, typename KeyType>
struct is_string_view_convertible_key_type<BasicJsonType, KeyTypeCVRef, KeyType, false>
: std::integral_constant < bool,
std::is_convertible<KeyTypeCVRef, std::string_view>::value
&& !is_usable_as_key_type<typename BasicJsonType::object_comparator_t,
typename BasicJsonType::object_t::key_type, KeyTypeCVRef, true, false>::value
&& is_usable_as_key_type<typename BasicJsonType::object_comparator_t,
typename BasicJsonType::object_t::key_type, std::string_view, true, false>::value > {};
#else
template<typename BasicJsonType, typename KeyTypeCVRef>
struct is_string_view_convertible_key_type : std::false_type {};
#endif
// type trait to check if KeyType can be used as an object key
// true if:
// - KeyType is comparable with BasicJsonType::object_t::key_type
@@ -4800,9 +4793,7 @@ using is_usable_as_basic_json_key_type = typename std::conditional <
typename BasicJsonType::object_t::key_type, KeyTypeCVRef,
RequireTransparentComparator, ExcludeObjectKeyType>::value
&& !is_json_iterator_of<BasicJsonType, KeyType>::value)
#ifdef JSON_HAS_CPP_17
|| std::is_convertible<KeyType, std::string_view>::value
#endif
|| is_string_view_convertible_key_type<BasicJsonType, KeyTypeCVRef>::value
, std::true_type,
std::false_type >::type;
@@ -27765,6 +27756,24 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
return it;
}
/// @brief the key to look up an object member with: the key itself, or its
/// std::string_view if the object can only be searched with that
template < typename KeyType, detail::enable_if_t <
!detail::is_string_view_convertible_key_type<basic_json_t, KeyType>::value, int > = 0 >
static KeyType && lookup_key(KeyType && key) noexcept
{
return std::forward<KeyType>(key);
}
#ifdef JSON_HAS_CPP_17
template < typename KeyType, detail::enable_if_t <
detail::is_string_view_convertible_key_type<basic_json_t, KeyType>::value, int > = 0 >
static std::string_view lookup_key(KeyType && key)
{
return std::forward<KeyType>(key);
}
#endif
/// @brief erase an element from the object and return the following one
/// Not every map returns an iterator from erase(iterator): some containers
/// (e.g., Abseil's hash maps) return void to avoid computing a successor
@@ -28085,27 +28094,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
}
}
/// @brief create the object type from a range, preserving @a src_object's
/// comparator when the object type supports it
/// Enabled for object types that provide a key_comp() and a matching
/// range-plus-comparator constructor, such as std::map. Other object
/// types, such as nlohmann::ordered_map, fall back to the plain range
/// constructor and default-construct their comparator, just as they
/// always have (@ref detail::is_comparator_constructible_object_type).
template<typename Iterator, detail::enable_if_t<
detail::is_comparator_constructible_object_type<object_t, Iterator>::value, int> = 0>
static object_t* create_object_with_comparator(const object_t& src_object, Iterator first, Iterator last)
{
return create<object_t>(first, last, src_object.key_comp());
}
template<typename Iterator, detail::enable_if_t<
detail::negation<detail::is_comparator_constructible_object_type<object_t, Iterator>>::value, int> = 0>
static object_t* create_object_with_comparator(const object_t& /*src_object*/, Iterator first, Iterator last)
{
return create<object_t>(first, last);
}
/// @brief create the copy of the object @a src in @a dst
/// @note structured values are appended to @a worklist instead
static void copy_object_level(const basic_json& src, basic_json& dst,
@@ -28123,8 +28111,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
scratch.emplace_back(element.first, basic_json());
}
dst.m_data.m_value.object = create_object_with_comparator(src_object,
std::make_move_iterator(scratch.begin()),
dst.m_data.m_value.object = create<object_t>(std::make_move_iterator(scratch.begin()),
std::make_move_iterator(scratch.end()));
// only now that the object exists may dst stop being a null value
dst.m_data.m_type = value_t::object;
@@ -29747,7 +29734,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", type_name()), this));
}
auto it = m_data.m_value.object->find(std::forward<KeyType>(key));
auto it = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
if (it == m_data.m_value.object->end())
{
JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(std::forward<KeyType>(key)), "' not found"), this));
@@ -29785,7 +29772,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", type_name()), this));
}
auto it = m_data.m_value.object->find(std::forward<KeyType>(key));
auto it = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
if (it == m_data.m_value.object->end())
{
JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(std::forward<KeyType>(key)), "' not found"), this));
@@ -29928,7 +29915,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
// operator[] only works for objects
if (JSON_HEDLEY_LIKELY(is_object()))
{
auto result = m_data.m_value.object->emplace(std::forward<KeyType>(key), nullptr);
auto result = m_data.m_value.object->emplace(lookup_key(std::forward<KeyType>(key)), nullptr);
return set_parent(result.first->second);
}
@@ -29944,7 +29931,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
// const operator[] only works for objects
if (JSON_HEDLEY_LIKELY(is_object()))
{
auto it = m_data.m_value.object->find(std::forward<KeyType>(key));
auto it = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
JSON_ASSERT(it != m_data.m_value.object->end());
return it->second;
}
@@ -29954,8 +29941,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
private:
template<typename KeyType>
using is_comparable_with_object_key = detail::is_comparable <
object_comparator_t, const typename object_t::key_type&, KeyType >;
using is_comparable_with_object_key = std::integral_constant < bool,
detail::is_comparable <
object_comparator_t, const typename object_t::key_type&, KeyType >::value
|| detail::is_string_view_convertible_key_type<basic_json_t, KeyType>::value >;
template<typename ValueType>
using value_return_type = std::conditional <
@@ -30342,7 +30331,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this));
}
const auto it = m_data.m_value.object->find(std::forward<KeyType>(key));
const auto it = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
if (it != m_data.m_value.object->end())
{
m_data.m_value.object->erase(it);
@@ -30369,7 +30358,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
detail::is_usable_as_basic_json_key_type<basic_json_t, KeyType>::value, int> = 0>
size_type erase(KeyType && key)
{
return erase_internal(std::forward<KeyType>(key));
return erase_internal(lookup_key(std::forward<KeyType>(key)));
}
/// @brief remove element from a JSON array given an index
@@ -30449,7 +30438,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
if (is_object())
{
result.m_it.object_iterator = m_data.m_value.object->find(std::forward<KeyType>(key));
result.m_it.object_iterator = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
}
return result;
@@ -30465,7 +30454,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
if (is_object())
{
result.m_it.object_iterator = m_data.m_value.object->find(std::forward<KeyType>(key));
result.m_it.object_iterator = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
}
return result;
@@ -30488,7 +30477,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
size_type count(KeyType && key) const
{
// return 0 for all nonobject types
return is_object() ? m_data.m_value.object->count(std::forward<KeyType>(key)) : 0;
return is_object() ? m_data.m_value.object->count(lookup_key(std::forward<KeyType>(key))) : 0;
}
/// @brief check the existence of an element in a JSON object
@@ -30506,7 +30495,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
JSON_HEDLEY_WARN_UNUSED_RESULT
bool contains(KeyType && key) const
{
return is_object() && m_data.m_value.object->find(std::forward<KeyType>(key)) != m_data.m_value.object->end();
return is_object() && m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key))) != m_data.m_value.object->end();
}
/// @brief check the existence of an element in a JSON object given a JSON pointer
-81
View File
@@ -827,46 +827,6 @@ Json nest(Json j, const std::size_t depth)
return j;
}
// a std::map comparator with state: case-insensitive, unless constructed
// case-sensitive. Used to check that copying an object copies the original's
// comparator rather than default-constructing a new one (see #5649).
struct key_case_less
{
key_case_less() = default;
explicit key_case_less(const bool cs) noexcept : case_sensitive(cs) {}
bool operator()(const std::string& a, const std::string& b) const
{
if (case_sensitive)
{
return a < b;
}
return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end(),
[](unsigned char x, unsigned char y)
{
return std::tolower(x) < std::tolower(y);
});
}
bool case_sensitive = false;
};
template<class Key, class Value, class /*Compare*/, class Allocator>
using key_case_map = std::map<Key, Value, key_case_less, Allocator>;
using key_case_json = nlohmann::basic_json<key_case_map>;
// the innermost value of a chain of single-element arrays
template<typename Json>
const Json& innermost(const Json& j)
{
const Json* p = &j;
while (p->is_array())
{
p = &(*p)[0];
}
return *p;
}
// orders keys case-insensitively, so "key" and "KEY" compare equivalent
// (neither less than the other) although they are not equal
struct case_insensitive_less
@@ -931,47 +891,6 @@ TEST_CASE("equality of objects whose entries have no fixed order")
}
}
TEST_CASE("copying an object preserves its comparator's state")
{
// Past the iterative deep copy's nesting bound, an object copy used to be
// built with a default-constructed comparator instead of a copy of the
// original's. For an object type whose comparator carries state - here, a
// std::map that compares keys case-sensitively only when created that way
// - this reordered the copy's keys and could even drop entries that the
// original's comparator kept distinct (see #5649).
key_case_json object = key_case_json::object_t(key_case_less(true)); // case-sensitive
object["b"] = 1;
object["B"] = 2;
object["a"] = 3;
REQUIRE(object.dump() == R"({"B":2,"a":3,"b":1})");
for (const std::size_t depth : std::vector<std::size_t> {0, 127, 128, 200})
{
CAPTURE(depth);
key_case_json original = object;
for (std::size_t i = 0; i < depth; ++i)
{
original = key_case_json::array({std::move(original)});
}
{
const key_case_json copy = original; // NOLINT(performance-unnecessary-copy-initialization)
CHECK(innermost(copy).size() == 3);
CHECK(innermost(copy).dump() == R"({"B":2,"a":3,"b":1})");
CHECK(copy == original);
}
{
key_case_json copy = key_case_json::array();
copy = original;
CHECK(innermost(copy).size() == 3);
CHECK(innermost(copy).dump() == R"({"B":2,"a":3,"b":1})");
CHECK(copy == original);
}
}
}
TEST_CASE("equality of an object whose comparator treats different keys as equivalent")
{
// https://github.com/nlohmann/json/issues/5655: past the nesting bound,
+111
View File
@@ -1973,4 +1973,115 @@ TEST_CASE("operator[] with user-defined std::string_view-convertible types")
}
}
}
TEST_CASE("keys convertible to std::string_view work with all lookup functions (regression test for #5663)")
{
// a key type convertible only to std::string_view: the case #4958 added
// support for, but only the non-const operator[] compiled with it
struct ViewKey
{
operator std::string_view() const
{
return "a";
}
};
// a key type convertible to both std::string and std::string_view: with
// 3.12.0, such a key worked with at, the const operator[], find, count and
// contains via the conversion to std::string; #4958 made the KeyType&&
// templates win overload resolution for it instead, and those then failed
struct DualKey
{
operator std::string() const
{
return "a";
}
operator std::string_view() const
{
return "a";
}
};
SECTION("nlohmann::json")
{
using json = nlohmann::json;
SECTION("ViewKey")
{
json j = {{"a", 1}};
const json& cj = j;
CHECK(j[ViewKey{}] == 1);
CHECK(cj[ViewKey{}] == 1);
CHECK(j.at(ViewKey{}) == 1);
CHECK(cj.at(ViewKey{}) == 1);
CHECK(j.find(ViewKey{}) != j.end());
CHECK(cj.find(ViewKey{}) != cj.end());
CHECK(j.count(ViewKey{}) == 1);
CHECK(j.contains(ViewKey{}));
CHECK(j.value(ViewKey{}, 0) == 1);
CHECK(j.erase(ViewKey{}) == 1);
CHECK(!j.contains("a"));
}
SECTION("DualKey")
{
json j = {{"a", 1}};
const json& cj = j;
CHECK(j[DualKey{}] == 1);
CHECK(cj[DualKey{}] == 1);
CHECK(j.at(DualKey{}) == 1);
CHECK(cj.at(DualKey{}) == 1);
CHECK(j.find(DualKey{}) != j.end());
CHECK(cj.find(DualKey{}) != cj.end());
CHECK(j.count(DualKey{}) == 1);
CHECK(j.contains(DualKey{}));
CHECK(j.value(DualKey{}, 0) == 1);
CHECK(j.erase(DualKey{}) == 1);
CHECK(!j.contains("a"));
}
}
SECTION("nlohmann::ordered_json")
{
using ordered_json = nlohmann::ordered_json;
SECTION("ViewKey")
{
ordered_json j = {{"a", 1}};
const ordered_json& cj = j;
CHECK(j[ViewKey{}] == 1);
CHECK(cj[ViewKey{}] == 1);
CHECK(j.at(ViewKey{}) == 1);
CHECK(cj.at(ViewKey{}) == 1);
CHECK(j.find(ViewKey{}) != j.end());
CHECK(cj.find(ViewKey{}) != cj.end());
CHECK(j.count(ViewKey{}) == 1);
CHECK(j.contains(ViewKey{}));
CHECK(j.value(ViewKey{}, 0) == 1);
CHECK(j.erase(ViewKey{}) == 1);
CHECK(!j.contains("a"));
}
SECTION("DualKey")
{
ordered_json j = {{"a", 1}};
const ordered_json& cj = j;
CHECK(j[DualKey{}] == 1);
CHECK(cj[DualKey{}] == 1);
CHECK(j.at(DualKey{}) == 1);
CHECK(cj.at(DualKey{}) == 1);
CHECK(j.find(DualKey{}) != j.end());
CHECK(cj.find(DualKey{}) != cj.end());
CHECK(j.count(DualKey{}) == 1);
CHECK(j.contains(DualKey{}));
CHECK(j.value(DualKey{}, 0) == 1);
CHECK(j.erase(DualKey{}) == 1);
CHECK(!j.contains("a"));
}
}
}
#endif