diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index cd731c290..99ec53585 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -1505,7 +1505,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 c54656207..913e9937b 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -28434,7 +28434,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