From b1c9a68b9bf30675e9623c767bc19f7747386087 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 28 Aug 2026 14:18:47 +0000 Subject: [PATCH] Fix object_comparator_t for object types without key_compare detail::actual_object_comparator selected between object_t::key_compare and default_object_comparator_t with std::conditional. Both type arguments of std::conditional are named eagerly, so object_t::key_compare had to exist regardless of the condition, and the has_key_compare guard added in 3.11.0 never took effect: any ObjectType without a key_compare member type failed to compile while instantiating basic_json itself. Use detected_or_t instead, which resolves through a SFINAE partial specialization and only names object_t::key_compare when it exists. The selected type is unchanged for every object type that compiled before, so object_comparator_t -- a public member type -- keeps its meaning and ABI. has_key_compare had no other users and is removed. Add a regression test using an adapter around std::unordered_map, which has no key_compare; it fails to compile without this change. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018hxZxz8svM54c6ATEvXp5E Signed-off-by: Niels Lohmann --- include/nlohmann/detail/meta/type_traits.hpp | 13 +-- single_include/nlohmann/json.hpp | 13 +-- tests/src/unit-custom-object-type.cpp | 102 +++++++++++++++++++ 3 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 tests/src/unit-custom-object-type.cpp diff --git a/include/nlohmann/detail/meta/type_traits.hpp b/include/nlohmann/detail/meta/type_traits.hpp index ebf6a2c26..36eaaab79 100644 --- a/include/nlohmann/detail/meta/type_traits.hpp +++ b/include/nlohmann/detail/meta/type_traits.hpp @@ -172,17 +172,18 @@ struct has_to_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> template using detect_key_compare = typename T::key_compare; -template -struct has_key_compare : std::integral_constant::value> {}; - -// obtains the actual object key comparator +// obtains the actual object key comparator: object_t::key_compare if the +// object type defines it, and default_object_comparator_t otherwise +// +// note detected_or_t is used rather than std::conditional, because the latter +// names both of its type arguments eagerly; object_t::key_compare would then +// be a hard error for an object type that does not define it template struct actual_object_comparator { using object_t = typename BasicJsonType::object_t; using object_comparator_t = typename BasicJsonType::default_object_comparator_t; - using type = typename std::conditional < has_key_compare::value, - typename object_t::key_compare, object_comparator_t>::type; + using type = detected_or_t; }; template diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 4c346b412..977c61414 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -3935,17 +3935,18 @@ struct has_to_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> template using detect_key_compare = typename T::key_compare; -template -struct has_key_compare : std::integral_constant::value> {}; - -// obtains the actual object key comparator +// obtains the actual object key comparator: object_t::key_compare if the +// object type defines it, and default_object_comparator_t otherwise +// +// note detected_or_t is used rather than std::conditional, because the latter +// names both of its type arguments eagerly; object_t::key_compare would then +// be a hard error for an object type that does not define it template struct actual_object_comparator { using object_t = typename BasicJsonType::object_t; using object_comparator_t = typename BasicJsonType::default_object_comparator_t; - using type = typename std::conditional < has_key_compare::value, - typename object_t::key_compare, object_comparator_t>::type; + using type = detected_or_t; }; template diff --git a/tests/src/unit-custom-object-type.cpp b/tests/src/unit-custom-object-type.cpp new file mode 100644 index 000000000..2349c0ecd --- /dev/null +++ b/tests/src/unit-custom-object-type.cpp @@ -0,0 +1,102 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +// An ObjectType that does *not* define a key_compare member type. It adapts +// std::unordered_map to the template argument order expected by basic_json, +// where the third argument is a comparator rather than a hash function. +template +struct unordered_map_object + : std::unordered_map, std::equal_to, Allocator> +{ + using base_t = std::unordered_map, std::equal_to, Allocator>; + using base_t::base_t; +}; + +using unordered_json = nlohmann::basic_json; + +} // namespace + +TEST_CASE("object type without key_compare") +{ + SECTION("object_comparator_t falls back to default_object_comparator_t") + { + CHECK(std::is_same < unordered_json::object_comparator_t, + unordered_json::default_object_comparator_t >::value); + } + + SECTION("object types defining key_compare are unaffected") + { + CHECK(std::is_same::value); + CHECK(std::is_same::value); + } + + SECTION("creating and accessing values") + { + unordered_json j; + j["one"] = 1; + j["two"] = "zwei"; + j["three"]["nested"] = true; + + CHECK(j.size() == 3); + CHECK(j.at("one") == 1); + CHECK(j["two"] == "zwei"); + CHECK(j["three"]["nested"] == true); + CHECK(j.contains("one")); + CHECK(!j.contains("four")); + CHECK(j.find("one") != j.end()); + CHECK(j.count("one") == 1); + CHECK(j.erase("one") == 1); + CHECK(j.size() == 2); + } + + SECTION("serialization and deserialization") + { + const auto j = unordered_json::parse(R"({"a":[1,2,3],"b":{"c":null}})"); + CHECK(j["a"].size() == 3); + CHECK(j["a"][2] == 3); + CHECK(j["b"]["c"].is_null()); + CHECK(unordered_json::parse(j.dump()) == j); + } + + SECTION("binary formats") + { + const auto j = unordered_json::parse(R"({"a":[1,2,3],"b":"x"})"); + CHECK(unordered_json::from_cbor(unordered_json::to_cbor(j)) == j); + CHECK(unordered_json::from_msgpack(unordered_json::to_msgpack(j)) == j); + } + + SECTION("conversion to and from nlohmann::json") + { + const auto j = unordered_json::parse(R"({"a":1,"b":[true,null]})"); + const nlohmann::json converted(j); + + CHECK(converted.is_object()); + CHECK(converted["a"] == 1); + CHECK(converted["b"][0] == true); + CHECK(converted["b"][1].is_null()); + CHECK(unordered_json(converted) == j); + } +}