Compare commits

..
Author SHA1 Message Date
Niels Lohmann a46d5216cf Merge branch 'develop' into claude/deep-copy-key-comp-5649
Conflicts:
- tests/src/unit-comparison.cpp: kept both sides' helpers (key_case_less/
  key_case_json/innermost from the PR, case_insensitive_less/ci_json from
  develop's #5720) and both new test cases

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-30 20:40:02 +02:00
Niels Lohmann 4eb8618d16 Keep astyle from padding the create_object_with_comparator templates
Spell the negated condition as detail::negation<...> instead of a leading
'!', which made astyle spread the template header out.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-30 18:17:33 +02:00
Niels Lohmann 6be8c2e986 Preserve the object comparator's state in a deep copy past the nesting bound
copy_object_level(), used by the copy constructor and copy assignment once a
value is nested deeper than the iterative deep copy's bound (128 levels, or
every copy under JSON_NO_THREAD_LOCAL), built each object's copy with the
object type's plain range constructor. That default-constructs the object's
comparator instead of copying the original's. For an object type whose
comparator carries state, such as a std::map that compares keys
case-sensitively only when constructed that way, the copy then ordered - and
could even deduplicate - its keys differently from the original.

Add detail::is_comparator_constructible_object_type, a detection trait for
object types that provide a key_comp() and a constructor taking a range and a
comparator, the way std::map does. copy_object_level now dispatches on it: an
object type that qualifies gets its copy built with src_object.key_comp()
passed along; other object types, such as nlohmann::ordered_map (which has a
key_compare for its std::map-like interface, but no key_comp()), keep using
the plain range constructor exactly as before.

merge_patch and update() were checked for the same pattern; neither is
affected, since both only ever add members one at a time to an object that
already has its own comparator (or start a brand new default-constructed one),
rather than rebuilding an object_t from a range copied out of an existing,
possibly custom-comparator object.

Fixes #5649.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-30 08:14:47 +02:00
12 changed files with 191 additions and 222 deletions
-1
View File
@@ -16,7 +16,6 @@ INSERT INTO searchIndex(name, type, path) VALUES ('basic_json', 'Class', 'api/ba
INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::accept', 'Function', 'api/basic_json/accept/index.html');
INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::array', 'Function', 'api/basic_json/array/index.html');
INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::array_t', 'Type', 'api/basic_json/array_t/index.html');
INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::as_base_class', 'Method', 'api/basic_json/as_base_class/index.html');
INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::at', 'Method', 'api/basic_json/at/index.html');
INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::back', 'Method', 'api/basic_json/back/index.html');
INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::basic_json', 'Constructor', 'api/basic_json/basic_json/index.html');
@@ -1,53 +0,0 @@
# <small>nlohmann::basic_json::</small>as_base_class
```cpp
json_base_class_t& as_base_class() noexcept;
const json_base_class_t& as_base_class() const noexcept;
```
Returns a reference to this object as its custom base class [`json_base_class_t`](json_base_class_t.md). No copy is
made.
Since `basic_json` derives from `json_base_class_t`, a member of `basic_json` hides any member of the custom base class
with the same name. This function makes such hidden members accessible again.
## Return value
reference to this object as [`json_base_class_t`](json_base_class_t.md)
## Exception safety
No-throw guarantee: this function never throws exceptions.
## Complexity
Constant.
## Notes
The function is equivalent to `static_cast<json_base_class_t&>(j)` (or `static_cast<const json_base_class_t&>(j)`).
## Examples
??? example
The example shows how to use `as_base_class` to access members of the custom base class that are hidden by members
of `basic_json`.
```cpp
--8<-- "examples/as_base_class.cpp"
```
Output:
```json
--8<-- "examples/as_base_class.output"
```
## See also
- [json_base_class_t](json_base_class_t.md) - type of the custom base class
## Version history
- Added in version 3.13.0.
-1
View File
@@ -200,7 +200,6 @@ Direct access to the stored value of a JSON value.
- [**get_ref**](get_ref.md) - get a reference value
- [**operator ValueType**](operator_ValueType.md) - get a value
- [**get_binary**](get_binary.md) - get a binary value
- [**as_base_class**](as_base_class.md) - access the custom base class
### Element access
@@ -27,18 +27,6 @@ A `CustomBaseClass` with non-static data members forfeits `basic_json`'s
[standard layout](https://en.cppreference.com/w/cpp/named_req/StandardLayoutType) guarantee. See
[Template Parameter Requirements](../../features/types/template_parameters.md#custombaseclass).
#### Name conflicts
Since `basic_json` derives from `CustomBaseClass`, members of `basic_json` hide members of `CustomBaseClass` with the
same name. Hidden members remain accessible via [`as_base_class`](as_base_class.md) or by casting the value to
`json_base_class_t`.
!!! warning "Avoid generic member names"
Future versions of the library may add members to `basic_json` that hide members of `CustomBaseClass` that are
accessible today. To reduce the risk of such conflicts, avoid generic names for the members of `CustomBaseClass`,
for instance by using a distinctive prefix.
## Examples
??? example
@@ -55,11 +43,6 @@ same name. Hidden members remain accessible via [`as_base_class`](as_base_class.
--8<-- "examples/json_base_class_t.output"
```
## See also
- [as_base_class](as_base_class.md) - access the custom base class
## Version history
- Added in version 3.12.0.
- Made a public member type in version 3.13.0; it was private before, so it could not be named outside the class.
@@ -1,41 +0,0 @@
#include <iostream>
#include <nlohmann/json.hpp>
class base_class_with_hidden_members
{
public:
const char* type_name() const noexcept
{
return "my_type_name";
}
std::size_t size() const noexcept
{
return 42;
}
};
using json = nlohmann::basic_json <
std::map,
std::vector,
std::string,
bool,
std::int64_t,
std::uint64_t,
double,
std::allocator,
nlohmann::adl_serializer,
std::vector<std::uint8_t>,
base_class_with_hidden_members
>;
int main()
{
json j = {1, 2, 3};
// the members of basic_json hide the members of the base class
std::cout << j.type_name() << ' ' << j.size() << '\n';
// access the hidden members of the base class
std::cout << j.as_base_class().type_name() << ' ' << j.as_base_class().size() << '\n';
}
@@ -1,2 +0,0 @@
array 3
my_type_name 42
-1
View File
@@ -114,7 +114,6 @@ nav:
- 'accept': api/basic_json/accept.md
- 'array': api/basic_json/array.md
- 'array_t': api/basic_json/array_t.md
- 'as_base_class': api/basic_json/as_base_class.md
- 'at': api/basic_json/at.md
- 'back': api/basic_json/back.md
- 'begin': api/basic_json/begin.md
@@ -189,6 +189,37 @@ struct actual_object_comparator
template<typename BasicJsonType>
using actual_object_comparator_t = typename actual_object_comparator<BasicJsonType>::type;
template<typename T>
using detect_key_comp = decltype(std::declval<const T&>().key_comp());
// whether ObjectType can be constructed from a pair of Iterator together with
// a copy of its own comparator, the way std::map can: it needs a nested
// key_compare, a const key_comp() convertible to it, and a matching
// (Iterator, Iterator, const key_compare&) constructor.
//
// used to preserve a stateful comparator when a copy is built from a range
// past the iterative deep copy's nesting bound (see copy_object_level); an
// object type that does not satisfy this, such as nlohmann::ordered_map
// (which has key_compare for its std::map-like interface, but no key_comp()),
// keeps default-constructing its comparator, just as it always has
template<typename ObjectType, typename Iterator, typename = void>
struct is_comparator_constructible_object_type_impl : std::false_type {};
template<typename ObjectType, typename Iterator>
struct is_comparator_constructible_object_type_impl <
ObjectType, Iterator, enable_if_t<is_detected<detect_key_compare, ObjectType>::value >>
{
using key_compare = typename ObjectType::key_compare;
static constexpr bool value =
is_detected_convertible<key_compare, detect_key_comp, ObjectType>::value &&
std::is_constructible<ObjectType, Iterator, Iterator, const key_compare&>::value;
};
template<typename ObjectType, typename Iterator>
struct is_comparator_constructible_object_type
: is_comparator_constructible_object_type_impl<ObjectType, Iterator> {};
/////////////////
// char_traits //
/////////////////
+24 -18
View File
@@ -155,6 +155,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
/// workaround type for MSVC
using basic_json_t = NLOHMANN_BASIC_JSON_TPL;
using json_base_class_t = ::nlohmann::detail::json_base_class<CustomBaseClass>;
JSON_PRIVATE_UNLESS_TESTED:
// convenience aliases for types residing in namespace detail;
@@ -214,9 +215,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
using cbor_tag_handler_t = detail::cbor_tag_handler_t;
/// how to encode BJData
using bjdata_version_t = detail::bjdata_version_t;
/// base class used to inject custom functionality into each instance of basic_json
/// @sa https://json.nlohmann.me/api/basic_json/json_base_class_t/
using json_base_class_t = ::nlohmann::detail::json_base_class<CustomBaseClass>;
/// helper type for initializer lists of basic_json values
using initializer_list_t = std::initializer_list<detail::json_ref<basic_json>>;
@@ -1129,6 +1127,27 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
}
}
/// @brief create the object type from a range, preserving @a src_object's
/// comparator when the object type supports it
/// Enabled for object types that provide a key_comp() and a matching
/// range-plus-comparator constructor, such as std::map. Other object
/// types, such as nlohmann::ordered_map, fall back to the plain range
/// constructor and default-construct their comparator, just as they
/// always have (@ref detail::is_comparator_constructible_object_type).
template<typename Iterator, detail::enable_if_t<
detail::is_comparator_constructible_object_type<object_t, Iterator>::value, int> = 0>
static object_t* create_object_with_comparator(const object_t& src_object, Iterator first, Iterator last)
{
return create<object_t>(first, last, src_object.key_comp());
}
template<typename Iterator, detail::enable_if_t<
detail::negation<detail::is_comparator_constructible_object_type<object_t, Iterator>>::value, int> = 0>
static object_t* create_object_with_comparator(const object_t& /*src_object*/, Iterator first, Iterator last)
{
return create<object_t>(first, last);
}
/// @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,
@@ -1146,7 +1165,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
scratch.emplace_back(element.first, basic_json());
}
dst.m_data.m_value.object = create<object_t>(std::make_move_iterator(scratch.begin()),
dst.m_data.m_value.object = create_object_with_comparator(src_object,
std::make_move_iterator(scratch.begin()),
std::make_move_iterator(scratch.end()));
// only now that the object exists may dst stop being a null value
dst.m_data.m_type = value_t::object;
@@ -2693,20 +2713,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
return *get_ptr<const binary_t*>();
}
/// @brief access the custom base class
/// @sa https://json.nlohmann.me/api/basic_json/as_base_class/
json_base_class_t& as_base_class() noexcept
{
return static_cast<json_base_class_t&>(*this);
}
/// @brief access the custom base class
/// @sa https://json.nlohmann.me/api/basic_json/as_base_class/
const json_base_class_t& as_base_class() const noexcept
{
return static_cast<const json_base_class_t&>(*this);
}
/// @}
////////////////////
+55 -18
View File
@@ -4197,6 +4197,37 @@ struct actual_object_comparator
template<typename BasicJsonType>
using actual_object_comparator_t = typename actual_object_comparator<BasicJsonType>::type;
template<typename T>
using detect_key_comp = decltype(std::declval<const T&>().key_comp());
// whether ObjectType can be constructed from a pair of Iterator together with
// a copy of its own comparator, the way std::map can: it needs a nested
// key_compare, a const key_comp() convertible to it, and a matching
// (Iterator, Iterator, const key_compare&) constructor.
//
// used to preserve a stateful comparator when a copy is built from a range
// past the iterative deep copy's nesting bound (see copy_object_level); an
// object type that does not satisfy this, such as nlohmann::ordered_map
// (which has key_compare for its std::map-like interface, but no key_comp()),
// keeps default-constructing its comparator, just as it always has
template<typename ObjectType, typename Iterator, typename = void>
struct is_comparator_constructible_object_type_impl : std::false_type {};
template<typename ObjectType, typename Iterator>
struct is_comparator_constructible_object_type_impl <
ObjectType, Iterator, enable_if_t<is_detected<detect_key_compare, ObjectType>::value >>
{
using key_compare = typename ObjectType::key_compare;
static constexpr bool value =
is_detected_convertible<key_compare, detect_key_comp, ObjectType>::value &&
std::is_constructible<ObjectType, Iterator, Iterator, const key_compare&>::value;
};
template<typename ObjectType, typename Iterator>
struct is_comparator_constructible_object_type
: is_comparator_constructible_object_type_impl<ObjectType, Iterator> {};
/////////////////
// char_traits //
/////////////////
@@ -27082,6 +27113,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
/// workaround type for MSVC
using basic_json_t = NLOHMANN_BASIC_JSON_TPL;
using json_base_class_t = ::nlohmann::detail::json_base_class<CustomBaseClass>;
JSON_PRIVATE_UNLESS_TESTED:
// convenience aliases for types residing in namespace detail;
@@ -27141,9 +27173,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
using cbor_tag_handler_t = detail::cbor_tag_handler_t;
/// how to encode BJData
using bjdata_version_t = detail::bjdata_version_t;
/// base class used to inject custom functionality into each instance of basic_json
/// @sa https://json.nlohmann.me/api/basic_json/json_base_class_t/
using json_base_class_t = ::nlohmann::detail::json_base_class<CustomBaseClass>;
/// helper type for initializer lists of basic_json values
using initializer_list_t = std::initializer_list<detail::json_ref<basic_json>>;
@@ -28056,6 +28085,27 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
}
}
/// @brief create the object type from a range, preserving @a src_object's
/// comparator when the object type supports it
/// Enabled for object types that provide a key_comp() and a matching
/// range-plus-comparator constructor, such as std::map. Other object
/// types, such as nlohmann::ordered_map, fall back to the plain range
/// constructor and default-construct their comparator, just as they
/// always have (@ref detail::is_comparator_constructible_object_type).
template<typename Iterator, detail::enable_if_t<
detail::is_comparator_constructible_object_type<object_t, Iterator>::value, int> = 0>
static object_t* create_object_with_comparator(const object_t& src_object, Iterator first, Iterator last)
{
return create<object_t>(first, last, src_object.key_comp());
}
template<typename Iterator, detail::enable_if_t<
detail::negation<detail::is_comparator_constructible_object_type<object_t, Iterator>>::value, int> = 0>
static object_t* create_object_with_comparator(const object_t& /*src_object*/, Iterator first, Iterator last)
{
return create<object_t>(first, last);
}
/// @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,
@@ -28073,7 +28123,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
scratch.emplace_back(element.first, basic_json());
}
dst.m_data.m_value.object = create<object_t>(std::make_move_iterator(scratch.begin()),
dst.m_data.m_value.object = create_object_with_comparator(src_object,
std::make_move_iterator(scratch.begin()),
std::make_move_iterator(scratch.end()));
// only now that the object exists may dst stop being a null value
dst.m_data.m_type = value_t::object;
@@ -29620,20 +29671,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
return *get_ptr<const binary_t*>();
}
/// @brief access the custom base class
/// @sa https://json.nlohmann.me/api/basic_json/as_base_class/
json_base_class_t& as_base_class() noexcept
{
return static_cast<json_base_class_t&>(*this);
}
/// @brief access the custom base class
/// @sa https://json.nlohmann.me/api/basic_json/as_base_class/
const json_base_class_t& as_base_class() const noexcept
{
return static_cast<const json_base_class_t&>(*this);
}
/// @}
////////////////////
+81
View File
@@ -827,6 +827,46 @@ Json nest(Json j, const std::size_t depth)
return j;
}
// a std::map comparator with state: case-insensitive, unless constructed
// case-sensitive. Used to check that copying an object copies the original's
// comparator rather than default-constructing a new one (see #5649).
struct key_case_less
{
key_case_less() = default;
explicit key_case_less(const bool cs) noexcept : case_sensitive(cs) {}
bool operator()(const std::string& a, const std::string& b) const
{
if (case_sensitive)
{
return a < b;
}
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);
});
}
bool case_sensitive = false;
};
template<class Key, class Value, class /*Compare*/, class Allocator>
using key_case_map = std::map<Key, Value, key_case_less, Allocator>;
using key_case_json = nlohmann::basic_json<key_case_map>;
// the innermost value of a chain of single-element arrays
template<typename Json>
const Json& innermost(const Json& j)
{
const Json* p = &j;
while (p->is_array())
{
p = &(*p)[0];
}
return *p;
}
// orders keys case-insensitively, so "key" and "KEY" compare equivalent
// (neither less than the other) although they are not equal
struct case_insensitive_less
@@ -891,6 +931,47 @@ TEST_CASE("equality of objects whose entries have no fixed order")
}
}
TEST_CASE("copying an object preserves its comparator's state")
{
// Past the iterative deep copy's nesting bound, an object copy used to be
// built with a default-constructed comparator instead of a copy of the
// original's. For an object type whose comparator carries state - here, a
// std::map that compares keys case-sensitively only when created that way
// - this reordered the copy's keys and could even drop entries that the
// original's comparator kept distinct (see #5649).
key_case_json object = key_case_json::object_t(key_case_less(true)); // case-sensitive
object["b"] = 1;
object["B"] = 2;
object["a"] = 3;
REQUIRE(object.dump() == R"({"B":2,"a":3,"b":1})");
for (const std::size_t depth : std::vector<std::size_t> {0, 127, 128, 200})
{
CAPTURE(depth);
key_case_json original = object;
for (std::size_t i = 0; i < depth; ++i)
{
original = key_case_json::array({std::move(original)});
}
{
const key_case_json copy = original; // NOLINT(performance-unnecessary-copy-initialization)
CHECK(innermost(copy).size() == 3);
CHECK(innermost(copy).dump() == R"({"B":2,"a":3,"b":1})");
CHECK(copy == original);
}
{
key_case_json copy = key_case_json::array();
copy = original;
CHECK(innermost(copy).size() == 3);
CHECK(innermost(copy).dump() == R"({"B":2,"a":3,"b":1})");
CHECK(copy == original);
}
}
}
TEST_CASE("equality of an object whose comparator treats different keys as equivalent")
{
// https://github.com/nlohmann/json/issues/5655: past the nesting bound,
-70
View File
@@ -10,8 +10,6 @@
#include <set>
#include <sstream>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include "doctest_compatibility.h"
@@ -407,71 +405,3 @@ TEST_CASE("JSON Visit Node")
);
CHECK(expected.empty());
}
// Test accessing members of a custom base class that are hidden by members of nlohmann::basic_json
class base_class_with_hidden_members
{
public:
const char* type_name() const noexcept // NOLINT(readability-convert-member-functions-to-static)
{
return "custom type_name";
}
std::size_t size() const noexcept
{
return m_size;
}
std::size_t m_size = 42;
};
using json_with_hidden_base_members =
nlohmann::basic_json <
std::map,
std::vector,
std::string,
bool,
std::int64_t,
std::uint64_t,
double,
std::allocator,
nlohmann::adl_serializer,
std::vector<std::uint8_t>,
base_class_with_hidden_members
>;
TEST_CASE("JSON Node as_base_class")
{
using json = json_with_hidden_base_members;
static_assert(std::is_same<decltype(std::declval<json&>().as_base_class()), json::json_base_class_t&>::value, "");
static_assert(std::is_same<decltype(std::declval<const json&>().as_base_class()), const json::json_base_class_t&>::value, "");
static_assert(noexcept(std::declval<json&>().as_base_class()), "");
static_assert(noexcept(std::declval<const json&>().as_base_class()), "");
SECTION("non-const")
{
json j = {1, 2, 3};
CHECK(std::string(j.type_name()) == "array");
CHECK(j.size() == 3);
CHECK(std::string(j.as_base_class().type_name()) == "custom type_name");
CHECK(j.as_base_class().size() == 42);
CHECK(&j.as_base_class() == &static_cast<json::json_base_class_t&>(j));
j.as_base_class().m_size = 7;
CHECK(j.as_base_class().size() == 7);
CHECK(j.size() == 3);
}
SECTION("const")
{
const json j = {1, 2, 3};
CHECK(std::string(j.type_name()) == "array");
CHECK(j.size() == 3);
CHECK(std::string(j.as_base_class().type_name()) == "custom type_name");
CHECK(j.as_base_class().size() == 42);
CHECK(&j.as_base_class() == &static_cast<const json::json_base_class_t&>(j));
}
}