Restore v3.12.0 support for custom object key types (#5795)

* Restore v3.12.0 support for custom object key types

Custom object_t types whose key_type is not string_t compiled with
v3.12.0 for several APIs that unreleased changes broke:

- to_bson failed for every custom key type (#5553 kept a const string_t*
  to the key); the nested entry's header is now written where the entry
  is found.
- Copying deep values (and parse, merge_patch, update, insert) required
  operator== on keys (#5389); keys without one are now paired via find().
- to_cbor/to_msgpack required an implicit conversion to string_t (#5746,
  #5328); keys without one go through a temporary basic_json again.
- at() required a conversion to string_t for its error message (#5727);
  other keys are passed to concat() unchanged again.

The new unit-custom-object-key-type.cpp covers five key types with
different capabilities.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Use the with_object_t alias for the custom object key test types

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Avoid floating-point equality in custom key type test

GCC with -Werror=float-equal rejects comparing the double value with ==.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Write the head of nested BSON elements in one helper

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Suppress bugprone-return-const-ref-from-parameter in key_for_message

The reference is only passed to concat() within the full-expression that
holds the key, like the similar helpers in binary_writer.hpp.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Make the value of the custom test key types private

clang-tidy (cppcoreguidelines-non-private-member-variables-in-classes)
rejects the protected member; the derived key types use a protected
accessor instead.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Pass keys with data() and size() unchanged into the at() miss message

key_for_message() converted every key that string_t can be constructed
from, so a miss on a string_t or string_view key copied it before
concat() copied it again. Keys that concat() can append through data()
and size() are now passed through; only other keys (string literals,
key types that just convert to string_t) are converted.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann authored and GitHub committed 2026-10-11 08:24:31 +02:00
1 parent 11ef352511
commit eb8899a29c
11 files changed
+855 -26

No files matched your search

+37 -2
View File
@@ -1421,6 +1421,24 @@ public:
return create<object_t>(first, last);
}
/// @brief compare two object keys for equality, if the key type supports it
/// @note object_t only needs operator< for its keys (std::map), so operator==
/// may not exist; the keys are then reported as different, which makes
/// copy_object_level pair the values via object_t::find()
template<typename K = typename object_t::key_type,
detail::enable_if_t<detail::is_detected<detail::detect_equal_comparable, K>::value, int> = 0>
static bool copy_keys_equal(const K& a, const K& b)
{
return a == b;
}
template < typename K = typename object_t::key_type,
detail::enable_if_t < !detail::is_detected<detail::detect_equal_comparable, K>::value, int > = 0 >
static bool copy_keys_equal(const K& /*a*/, const K& /*b*/)
{
return false;
}
/// @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,
@@ -1453,7 +1471,7 @@ public:
auto src_it = src_object.cbegin();
for (auto& element : *dst.m_data.m_value.object)
{
if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && src_it->first == element.first))
if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && copy_keys_equal(src_it->first, element.first)))
{
copy_shallow(src_it->second, element.second, worklist);
++src_it;
@@ -3330,11 +3348,28 @@ public:
// std::map or ordered_map) never moves from its argument, so key is still
// valid here regardless of whether KeyType was deduced as an rvalue reference
// NOLINTNEXTLINE(bugprone-use-after-move,hicpp-invalid-access-moved)
JSON_THROW(out_of_range::create(detail::exception_id::key_not_found, detail::concat("key '", string_t(key), "' not found"), &j));
JSON_THROW(out_of_range::create(detail::exception_id::key_not_found, detail::concat("key '", key_for_message(key), "' not found"), &j));
}
return it->second;
}
/// @brief key as it is passed to detail::concat for an error message
/// @note keys with data() and size() (such as string_t itself or a string
/// view) are passed through unchanged, so a miss does not copy them;
/// other keys (such as string literals or key types that only convert
/// to string_t) are converted to string_t
template < typename KeyType, detail::enable_if_t < detail::detect_string_can_append_data<string_t, KeyType>::value, int > = 0 >
static const KeyType & key_for_message(const KeyType& key)
{
return key; // NOLINT(bugprone-return-const-ref-from-parameter): the result is only passed to concat() within the full-expression that holds key
}
template < typename KeyType, detail::enable_if_t < !detail::detect_string_can_append_data<string_t, KeyType>::value, int > = 0 >
static string_t key_for_message(const KeyType& key)
{
return string_t(key);
}
/// @brief checked array element access used by the at() overloads taking an index
/// @throw type_error.304 if @a j is not an array
/// @throw out_of_range.401 if @a idx is out of range