Require the found key to equal the looked-up key when comparing objects (#5720)

For an object type whose comparator treats unequal keys as equivalent
(for example a std::map with a case-insensitive comparator),
compare_iteratively() looked up a mismatched left key in the right
object with find(), which uses the object's own comparator, and
accepted whatever entry it found without checking that the keys are
actually equal. A case-insensitive comparator then found "KEY" for
"key", so two objects nested past the recursion bound (or at every
depth with JSON_NO_THREAD_LOCAL) could compare equal even though the
object type's own operator== - and basic_json itself, below the bound
- consider them different.

Accept the found entry only if its key equals (not just compares
equivalent to) the looked-up key.

Fixes #5655.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann
2026-09-30 20:07:56 +02:00
committed by GitHub
parent 66877675b1
commit 2ea6d8c127
3 changed files with 52 additions and 2 deletions
+3 -1
View File
@@ -1505,7 +1505,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
const auto found = (!Ordered && !detail::is_ordered_map<object_t>::value) const auto found = (!Ordered && !detail::is_ordered_map<object_t>::value)
? rhs_object->find(current.lhs_object_it->first) ? rhs_object->find(current.lhs_object_it->first)
: rhs_object->cend(); : rhs_object->cend();
if (found == rhs_object->cend()) // the object's comparator may find an entry whose
// key is only equivalent, not equal, to this one
if (found == rhs_object->cend() || !(found->first == current.lhs_object_it->first))
{ {
return key_result; return key_result;
} }
+3 -1
View File
@@ -28434,7 +28434,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
const auto found = (!Ordered && !detail::is_ordered_map<object_t>::value) const auto found = (!Ordered && !detail::is_ordered_map<object_t>::value)
? rhs_object->find(current.lhs_object_it->first) ? rhs_object->find(current.lhs_object_it->first)
: rhs_object->cend(); : rhs_object->cend();
if (found == rhs_object->cend()) // the object's comparator may find an entry whose
// key is only equivalent, not equal, to this one
if (found == rhs_object->cend() || !(found->first == current.lhs_object_it->first))
{ {
return key_result; return key_result;
} }
+46
View File
@@ -17,6 +17,7 @@
#include <algorithm> #include <algorithm>
#include <cctype>
#include <cstdint> #include <cstdint>
#include <map> #include <map>
#include <string> #include <string>
@@ -825,6 +826,24 @@ Json nest(Json j, const std::size_t depth)
} }
return j; return j;
} }
// orders keys case-insensitively, so "key" and "KEY" compare equivalent
// (neither less than the other) although they are not equal
struct case_insensitive_less
{
bool operator()(const std::string& a, const std::string& b) const
{
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);
});
}
};
template<class Key, class Value, class /*Compare*/, class Allocator>
using case_insensitive_map = std::map<Key, Value, case_insensitive_less, Allocator>;
using ci_json = nlohmann::basic_json<case_insensitive_map>;
} // namespace } // namespace
TEST_CASE("equality of objects whose entries have no fixed order") TEST_CASE("equality of objects whose entries have no fixed order")
@@ -872,6 +891,33 @@ TEST_CASE("equality of objects whose entries have no fixed order")
} }
} }
TEST_CASE("equality of an object whose comparator treats different keys as equivalent")
{
// https://github.com/nlohmann/json/issues/5655: past the nesting bound,
// the entries are compared without the call stack, and a key that finds
// no counterpart at the same position is looked up with find(), which
// uses the object's own comparator. A case-insensitive comparator then
// finds "KEY" for "key" and must not accept that pair as a match - the
// object type's own operator==, like std::map's, compares keys with ==.
ci_json a = ci_json::object();
a["key"] = 1;
ci_json b = ci_json::object();
b["KEY"] = 1;
// sanity check: the object type's own comparison already disagrees
CHECK_FALSE(a.get_ref<const ci_json::object_t&>() == b.get_ref<const ci_json::object_t&>());
for (const std::size_t depth : std::vector<std::size_t> {0, 127, 128, 200})
{
CAPTURE(depth);
const ci_json x = nest(a, depth);
const ci_json y = nest(b, depth);
CHECK_FALSE(x == y);
CHECK(x != y);
}
}
TEST_CASE("containers are compared element by element") TEST_CASE("containers are compared element by element")
{ {
// Containers nested deeper than a bound are compared without the call // Containers nested deeper than a bound are compared without the call