From 34f46246046337b05cfe3224e514e8dea4d9432d Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 30 Sep 2026 08:07:52 +0200 Subject: [PATCH] Require the found key to equal the looked-up key when comparing objects 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 --- include/nlohmann/json.hpp | 4 ++- single_include/nlohmann/json.hpp | 4 ++- tests/src/unit-comparison.cpp | 46 ++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 500fcddf2..ecf599ec2 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -1507,7 +1507,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec const auto found = (!Ordered && !detail::is_ordered_map::value) ? rhs_object->find(current.lhs_object_it->first) : 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; } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 576498738..c14f84111 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -27588,7 +27588,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec const auto found = (!Ordered && !detail::is_ordered_map::value) ? rhs_object->find(current.lhs_object_it->first) : 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; } diff --git a/tests/src/unit-comparison.cpp b/tests/src/unit-comparison.cpp index 69c0103c9..6dcec8390 100644 --- a/tests/src/unit-comparison.cpp +++ b/tests/src/unit-comparison.cpp @@ -17,6 +17,7 @@ #include +#include #include #include #include @@ -825,6 +826,24 @@ Json nest(Json j, const std::size_t depth) } 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 +using case_insensitive_map = std::map; +using ci_json = nlohmann::basic_json; } // namespace 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() == b.get_ref()); + + for (const std::size_t depth : std::vector {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") { // Containers nested deeper than a bound are compared without the call