From 0db359cabcd32f70ae0ac23f3ab821351d969404 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 9 Oct 2026 14:37:07 +0200 Subject: [PATCH] Restore v3.12.0 support for custom object key types Custom object_t types whose key_type is not string_t compiled with v3.12.0 for several APIs that unreleased changes broke: - to_bson failed for every custom key type (#5553 kept a const string_t* to the key); the nested entry's header is now written where the entry is found. - Copying deep values (and parse, merge_patch, update, insert) required operator== on keys (#5389); keys without one are now paired via find(). - to_cbor/to_msgpack required an implicit conversion to string_t (#5746, #5328); keys without one go through a temporary basic_json again. - at() required a conversion to string_t for its error message (#5727); other keys are passed to concat() unchanged again. The new unit-custom-object-key-type.cpp covers five key types with different capabilities. Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/object_t.md | 3 +- include/nlohmann/detail/meta/type_traits.hpp | 4 + .../nlohmann/detail/output/binary_writer.hpp | 66 ++- include/nlohmann/json.hpp | 38 +- single_include/nlohmann/json.hpp | 108 +++- tests/src/unit-custom-object-key-type.cpp | 501 ++++++++++++++++++ 6 files changed, 672 insertions(+), 48 deletions(-) create mode 100644 tests/src/unit-custom-object-key-type.cpp diff --git a/docs/mkdocs/docs/api/basic_json/object_t.md b/docs/mkdocs/docs/api/basic_json/object_t.md index 5e7cab9e9..204e5ab28 100644 --- a/docs/mkdocs/docs/api/basic_json/object_t.md +++ b/docs/mkdocs/docs/api/basic_json/object_t.md @@ -26,8 +26,7 @@ To store objects in C++, a type is defined by the template parameters described `StringType` : the type of the keys or names (e.g., `std::string`). The comparison function `std::less` is used to - order elements inside the container. `object_t::key_type` must be implicitly convertible to `string_t` (required by the - binary formats). + order elements inside the container. `AllocatorType` : the allocator to use for objects (e.g., `std::allocator`) diff --git a/include/nlohmann/detail/meta/type_traits.hpp b/include/nlohmann/detail/meta/type_traits.hpp index 4c90c68a3..a08ad1500 100644 --- a/include/nlohmann/detail/meta/type_traits.hpp +++ b/include/nlohmann/detail/meta/type_traits.hpp @@ -172,6 +172,10 @@ struct has_to_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> template using detect_key_compare = typename T::key_compare; +// detects whether two values of type T can be compared with operator== +template +using detect_equal_comparable = decltype(static_cast(std::declval() == std::declval())); + // obtains the actual object key comparator: object_t::key_compare if the // object type defines it, and default_object_comparator_t otherwise // diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index 4af3ff9fb..83bdc4afe 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -307,12 +307,6 @@ class binary_writer case value_t::object: { - static_assert( - std::is_convertible < - typename BasicJsonType::object_t::key_type, - string_t >::value, - "object_t::key_type must be implicitly convertible to string_t"); - // step 1: write control byte and the object size write_cbor_head(0xA0, j.m_data.m_value.object->size()); @@ -320,7 +314,7 @@ class binary_writer { // el.first is written directly (not via a temporary // basic_json), with the object as diagnostics context - write_cbor_string(el.first, j); + write_cbor_key(el.first, j); write_cbor(el.second, depth + 1); } break; @@ -585,12 +579,6 @@ class binary_writer case value_t::object: { - static_assert( - std::is_convertible < - typename BasicJsonType::object_t::key_type, - string_t >::value, - "object_t::key_type must be implicitly convertible to string_t"); - // step 1: write control byte and the object size write_msgpack_object_prefix(j.m_data.m_value.object->size(), j); @@ -598,7 +586,7 @@ class binary_writer { // as in write_cbor, el.first is written directly with the // object as diagnostics context - write_msgpack_string(el.first, j); + write_msgpack_key(el.first, j); write_msgpack(el.second, depth + 1); } break; @@ -984,7 +972,7 @@ class binary_writer // the key is written directly (not via a temporary basic_json), // with the object as diagnostics context, as in write_cbor - write_cbor_string(current.object_it->first, *current.value); + write_cbor_key(current.object_it->first, *current.value); const BasicJsonType* child = &(current.object_it->second); ++stack.back().object_it; write_cbor_value_or_push(*child, stack); @@ -1061,7 +1049,7 @@ class binary_writer // as in write_cbor_iterative, the key is written directly with // the object as diagnostics context - write_msgpack_string(current.object_it->first, *current.value); + write_msgpack_key(current.object_it->first, *current.value); const BasicJsonType* child = &(current.object_it->second); ++stack.back().object_it; write_msgpack_value_or_push(*child, stack); @@ -1828,7 +1816,6 @@ class binary_writer { // write entries until the current object or array is done, or an // entry is an object or array itself - const string_t* nested_name = nullptr; const BasicJsonType* nested = nullptr; if (current.value->is_object()) { @@ -1839,7 +1826,8 @@ class binary_writer ++current.member; if (el.second.is_structured()) { - nested_name = &el.first; + write_bson_entry_header(el.first, el.second.is_object() ? 0x03 : 0x04); + write_number(to_bson_length(nested_sizes[next_size++]), true); nested = &el.second; } else @@ -1858,7 +1846,8 @@ class binary_writer ++current.index; if (el.is_structured()) { - nested_name = &index_name; + write_bson_entry_header(index_name, el.is_object() ? 0x03 : 0x04); + write_number(to_bson_length(nested_sizes[next_size++]), true); nested = ⪙ } else @@ -1870,8 +1859,6 @@ class binary_writer if (nested != nullptr) { - write_bson_entry_header(*nested_name, nested->is_object() ? 0x03 : 0x04); - write_number(to_bson_length(nested_sizes[next_size++]), true); parents.push_back(std::move(current)); current = bson_frame(nested); continue; @@ -1939,6 +1926,43 @@ class binary_writer } } + /*! + @brief write an object key as a CBOR text string + + A key convertible to string_t is written directly. Other key types (only + an explicit conversion, or only a to_json overload) go through a temporary + basic_json, as in version 3.12.0; the temporary is then the diagnostics + context for strict UTF-8 checks. + */ + template::value, int> = 0> + void write_cbor_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& context) + { + write_cbor_string(key, context); + } + + template < typename Key = typename BasicJsonType::object_t::key_type, + enable_if_t < !std::is_convertible::value, int > = 0 > + void write_cbor_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& /*context*/) + { + write_cbor(BasicJsonType(key)); + } + + /// @brief write an object key as a MessagePack str, as in @ref write_cbor_key + template::value, int> = 0> + void write_msgpack_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& context) + { + write_msgpack_string(key, context); + } + + template < typename Key = typename BasicJsonType::object_t::key_type, + enable_if_t < !std::is_convertible::value, int > = 0 > + void write_msgpack_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& /*context*/) + { + write_msgpack(BasicJsonType(key)); + } + /*! @brief write a CBOR text string diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 9c89ab0bc..6fa3cf98a 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -1421,6 +1421,24 @@ public: return create(first, last); } + /// @brief compare two object keys for equality, if the key type supports it + /// @note object_t only needs operator< for its keys (std::map), so operator== + /// may not exist; the keys are then reported as different, which makes + /// copy_object_level pair the values via object_t::find() + template::value, int> = 0> + static bool copy_keys_equal(const K& a, const K& b) + { + return a == b; + } + + template < typename K = typename object_t::key_type, + detail::enable_if_t < !detail::is_detected::value, int > = 0 > + static bool copy_keys_equal(const K& /*a*/, const K& /*b*/) + { + return false; + } + /// @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, @@ -1453,7 +1471,7 @@ public: auto src_it = src_object.cbegin(); for (auto& element : *dst.m_data.m_value.object) { - if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && src_it->first == element.first)) + if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && copy_keys_equal(src_it->first, element.first))) { copy_shallow(src_it->second, element.second, worklist); ++src_it; @@ -3330,11 +3348,27 @@ public: // std::map or ordered_map) never moves from its argument, so key is still // valid here regardless of whether KeyType was deduced as an rvalue reference // NOLINTNEXTLINE(bugprone-use-after-move,hicpp-invalid-access-moved) - JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(key), "' not found"), &j)); + JSON_THROW(out_of_range::create(403, detail::concat("key '", key_for_message(key), "' not found"), &j)); } return it->second; } + /// @brief key as it is passed to detail::concat for an error message + /// @note string_t is used where it can be constructed from the key; other + /// key types are passed through unchanged, as concat only needs + /// data() and size() of them + template::value, int> = 0> + static string_t key_for_message(const KeyType& key) + { + return string_t(key); + } + + template < typename KeyType, detail::enable_if_t < !std::is_constructible::value, int > = 0 > + static const KeyType & key_for_message(const KeyType& key) + { + return key; + } + /// @brief checked array element access used by the at() overloads taking an index /// @throw type_error.304 if @a j is not an array /// @throw out_of_range.401 if @a idx is out of range diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 428a6abbf..badb1dc39 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -4171,6 +4171,10 @@ struct has_to_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> template using detect_key_compare = typename T::key_compare; +// detects whether two values of type T can be compared with operator== +template +using detect_equal_comparable = decltype(static_cast(std::declval() == std::declval())); + // obtains the actual object key comparator: object_t::key_compare if the // object type defines it, and default_object_comparator_t otherwise // @@ -21812,12 +21816,6 @@ class binary_writer case value_t::object: { - static_assert( - std::is_convertible < - typename BasicJsonType::object_t::key_type, - string_t >::value, - "object_t::key_type must be implicitly convertible to string_t"); - // step 1: write control byte and the object size write_cbor_head(0xA0, j.m_data.m_value.object->size()); @@ -21825,7 +21823,7 @@ class binary_writer { // el.first is written directly (not via a temporary // basic_json), with the object as diagnostics context - write_cbor_string(el.first, j); + write_cbor_key(el.first, j); write_cbor(el.second, depth + 1); } break; @@ -22090,12 +22088,6 @@ class binary_writer case value_t::object: { - static_assert( - std::is_convertible < - typename BasicJsonType::object_t::key_type, - string_t >::value, - "object_t::key_type must be implicitly convertible to string_t"); - // step 1: write control byte and the object size write_msgpack_object_prefix(j.m_data.m_value.object->size(), j); @@ -22103,7 +22095,7 @@ class binary_writer { // as in write_cbor, el.first is written directly with the // object as diagnostics context - write_msgpack_string(el.first, j); + write_msgpack_key(el.first, j); write_msgpack(el.second, depth + 1); } break; @@ -22489,7 +22481,7 @@ class binary_writer // the key is written directly (not via a temporary basic_json), // with the object as diagnostics context, as in write_cbor - write_cbor_string(current.object_it->first, *current.value); + write_cbor_key(current.object_it->first, *current.value); const BasicJsonType* child = &(current.object_it->second); ++stack.back().object_it; write_cbor_value_or_push(*child, stack); @@ -22566,7 +22558,7 @@ class binary_writer // as in write_cbor_iterative, the key is written directly with // the object as diagnostics context - write_msgpack_string(current.object_it->first, *current.value); + write_msgpack_key(current.object_it->first, *current.value); const BasicJsonType* child = &(current.object_it->second); ++stack.back().object_it; write_msgpack_value_or_push(*child, stack); @@ -23333,7 +23325,6 @@ class binary_writer { // write entries until the current object or array is done, or an // entry is an object or array itself - const string_t* nested_name = nullptr; const BasicJsonType* nested = nullptr; if (current.value->is_object()) { @@ -23344,7 +23335,8 @@ class binary_writer ++current.member; if (el.second.is_structured()) { - nested_name = &el.first; + write_bson_entry_header(el.first, el.second.is_object() ? 0x03 : 0x04); + write_number(to_bson_length(nested_sizes[next_size++]), true); nested = &el.second; } else @@ -23363,7 +23355,8 @@ class binary_writer ++current.index; if (el.is_structured()) { - nested_name = &index_name; + write_bson_entry_header(index_name, el.is_object() ? 0x03 : 0x04); + write_number(to_bson_length(nested_sizes[next_size++]), true); nested = ⪙ } else @@ -23375,8 +23368,6 @@ class binary_writer if (nested != nullptr) { - write_bson_entry_header(*nested_name, nested->is_object() ? 0x03 : 0x04); - write_number(to_bson_length(nested_sizes[next_size++]), true); parents.push_back(std::move(current)); current = bson_frame(nested); continue; @@ -23444,6 +23435,43 @@ class binary_writer } } + /*! + @brief write an object key as a CBOR text string + + A key convertible to string_t is written directly. Other key types (only + an explicit conversion, or only a to_json overload) go through a temporary + basic_json, as in version 3.12.0; the temporary is then the diagnostics + context for strict UTF-8 checks. + */ + template::value, int> = 0> + void write_cbor_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& context) + { + write_cbor_string(key, context); + } + + template < typename Key = typename BasicJsonType::object_t::key_type, + enable_if_t < !std::is_convertible::value, int > = 0 > + void write_cbor_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& /*context*/) + { + write_cbor(BasicJsonType(key)); + } + + /// @brief write an object key as a MessagePack str, as in @ref write_cbor_key + template::value, int> = 0> + void write_msgpack_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& context) + { + write_msgpack_string(key, context); + } + + template < typename Key = typename BasicJsonType::object_t::key_type, + enable_if_t < !std::is_convertible::value, int > = 0 > + void write_msgpack_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& /*context*/) + { + write_msgpack(BasicJsonType(key)); + } + /*! @brief write a CBOR text string @@ -29160,6 +29188,24 @@ public: return create(first, last); } + /// @brief compare two object keys for equality, if the key type supports it + /// @note object_t only needs operator< for its keys (std::map), so operator== + /// may not exist; the keys are then reported as different, which makes + /// copy_object_level pair the values via object_t::find() + template::value, int> = 0> + static bool copy_keys_equal(const K& a, const K& b) + { + return a == b; + } + + template < typename K = typename object_t::key_type, + detail::enable_if_t < !detail::is_detected::value, int > = 0 > + static bool copy_keys_equal(const K& /*a*/, const K& /*b*/) + { + return false; + } + /// @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, @@ -29192,7 +29238,7 @@ public: auto src_it = src_object.cbegin(); for (auto& element : *dst.m_data.m_value.object) { - if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && src_it->first == element.first)) + if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && copy_keys_equal(src_it->first, element.first))) { copy_shallow(src_it->second, element.second, worklist); ++src_it; @@ -31069,11 +31115,27 @@ public: // std::map or ordered_map) never moves from its argument, so key is still // valid here regardless of whether KeyType was deduced as an rvalue reference // NOLINTNEXTLINE(bugprone-use-after-move,hicpp-invalid-access-moved) - JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(key), "' not found"), &j)); + JSON_THROW(out_of_range::create(403, detail::concat("key '", key_for_message(key), "' not found"), &j)); } return it->second; } + /// @brief key as it is passed to detail::concat for an error message + /// @note string_t is used where it can be constructed from the key; other + /// key types are passed through unchanged, as concat only needs + /// data() and size() of them + template::value, int> = 0> + static string_t key_for_message(const KeyType& key) + { + return string_t(key); + } + + template < typename KeyType, detail::enable_if_t < !std::is_constructible::value, int > = 0 > + static const KeyType & key_for_message(const KeyType& key) + { + return key; + } + /// @brief checked array element access used by the at() overloads taking an index /// @throw type_error.304 if @a j is not an array /// @throw out_of_range.401 if @a idx is out of range diff --git a/tests/src/unit-custom-object-key-type.cpp b/tests/src/unit-custom-object-key-type.cpp new file mode 100644 index 000000000..4bf5182fa --- /dev/null +++ b/tests/src/unit-custom-object-key-type.cpp @@ -0,0 +1,501 @@ +// __ _____ _____ _____ +// __| | __| | | | 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 + +// Object types with a user-defined key type. The key types differ in what they +// offer to the library: a conversion to std::string (implicit or explicit), a +// comparison with ==, a to_json overload, or a c_str() member. + +namespace custom_key_test +{ +class key_base +{ + public: + key_base() = default; + + key_base(const char* value) + : m_value(value) + {} + + key_base(std::string value) + : m_value(std::move(value)) + {} + + // Required by JSON_DIAGNOSTICS, which reads object keys through data() + // when building the path of an exception. + const char* data() const noexcept + { + return m_value.data(); + } + + friend bool operator<(const key_base& lhs, const key_base& rhs) + { + return lhs.m_value < rhs.m_value; + } + + protected: + std::string m_value; +}; + +// implicit conversion to std::string and operator== +class key_full : public key_base +{ + public: + key_full() = default; + using key_base::key_base; + + operator std::string() const + { + return m_value; + } + + friend bool operator==(const key_full& lhs, const key_full& rhs) + { + return lhs.m_value == rhs.m_value; + } +}; + +// implicit conversion to std::string, but no operator== +class key_no_eq : public key_base +{ + public: + key_no_eq() = default; + using key_base::key_base; + + operator std::string() const + { + return m_value; + } +}; + +// explicit conversion to std::string, no operator== +class key_explicit : public key_base +{ + public: + key_explicit() = default; + using key_base::key_base; + + explicit operator std::string() const + { + return m_value; + } +}; + +// no conversion at all, only a to_json overload, no operator== +class key_to_json : public key_base +{ + public: + key_to_json() = default; + using key_base::key_base; + + const std::string& value() const + { + return m_value; + } +}; + +template +void to_json(BasicJsonType& j, const key_to_json& k) +{ + j = k.value(); +} + +// like key_to_json, but with size() and c_str() +class key_c_str : public key_base +{ + public: + key_c_str() = default; + using key_base::key_base; + + const std::string& value() const + { + return m_value; + } + + std::size_t size() const + { + return m_value.size(); + } + + const char* c_str() const + { + return m_value.c_str(); + } +}; + +template +void to_json(BasicJsonType& j, const key_c_str& k) +{ + j = k.value(); +} + +// std::map with key type K, ignoring the key type basic_json passes +template +struct object_for +{ + template + using pair_allocator = typename std::allocator_traits::template rebind_alloc>; + + template + using type = std::map, pair_allocator>; // NOLINT(modernize-use-transparent-functors) +}; + +using json_full = nlohmann::basic_json::type>; +using json_no_eq = nlohmann::basic_json::type>; +using json_explicit = nlohmann::basic_json::type>; +using json_to_json = nlohmann::basic_json::type>; +using json_c_str = nlohmann::basic_json::type>; + +// a key that is long enough to need a length byte in CBOR and MessagePack +const char* long_key_name(std::size_t i, std::string& storage); +const char* long_key_name(std::size_t i, std::string& storage) +{ + storage = "a key longer than thirty-one characters " + std::to_string(i); + return storage.c_str(); +} + +// name of the key at nesting level i of a deep value +std::string deep_name(std::size_t i, bool long_keys); +std::string deep_name(std::size_t i, bool long_keys) +{ + std::string storage; + return (long_keys && i % 2 == 1) ? std::string(long_key_name(i, storage)) : "k" + std::to_string(i); +} + +// {"a": 1, "b": [true, null, "x"], "c": {"d": 2.5}, } +// 23 is the longest CBOR length stored in the initial byte; 36 needs one +// length byte in CBOR and MessagePack, 300 needs two +template +J make_shallow() +{ + using key_t = typename J::object_t::key_type; + + J array = J::array(); + array.push_back(J(true)); + array.push_back(J(nullptr)); + array.push_back(J("x")); + + typename J::object_t inner; + inner.emplace(key_t("d"), J(2.5)); + + typename J::object_t object; + object.emplace(key_t("a"), J(1)); + object.emplace(key_t("b"), std::move(array)); + object.emplace(key_t("c"), J(std::move(inner))); + object.emplace(key_t(std::string(23, 'x')), J(2)); + object.emplace(key_t(std::string(36, 'y')), J(3)); + object.emplace(key_t(std::string(300, 'z')), J(4)); + return J(std::move(object)); +} + +// {"k0": {"k1": {... {"k": 1} ...}}} +template +J make_deep(std::size_t depth, bool long_keys) +{ + using key_t = typename J::object_t::key_type; + + J value = 1; + for (std::size_t i = depth; i > 0; --i) + { + typename J::object_t object; + object.emplace(key_t(deep_name(i - 1, long_keys)), std::move(value)); + value = J(std::move(object)); + } + return value; +} + +std::size_t deep_depth(); +std::size_t deep_depth() +{ + return nlohmann::detail::recursion_depth_limit() + 10; +} + +// walk down the nesting levels without recursion and check the leaf +template +bool check_deep(const J& value, std::size_t depth, bool long_keys) +{ + using key_t = typename J::object_t::key_type; + + const J* current = &value; + for (std::size_t i = 0; i < depth; ++i) + { + if (!current->is_object() || current->size() != 1) + { + return false; + } + const auto it = current->find(key_t(deep_name(i, long_keys))); + if (it == current->end()) + { + return false; + } + current = &it.value(); + } + return current->is_number_integer() && current->template get() == 1; +} + +template +bool check_shallow(const J& value) +{ + using key_t = typename J::object_t::key_type; + + if (!value.is_object() || value.size() != 6) + { + return false; + } + + const auto a = value.find(key_t("a")); + const auto b = value.find(key_t("b")); + const auto c = value.find(key_t("c")); + if (a == value.end() || b == value.end() || c == value.end()) + { + return false; + } + + const auto d = c->find(key_t("d")); + // basic_json::operator== needs operator== on the keys, which most of the + // key types do not have, so the values are checked through get<>() + return a->template get() == 1 + && b->is_array() && b->size() == 3 && (*b)[0].template get() && (*b)[1].is_null() + && (*b)[2].template get() == "x" + && d != c->end() && d->template get() == 2.5 + && value.find(key_t(std::string(23, 'x')))->template get() == 2 + && value.find(key_t(std::string(36, 'y')))->template get() == 3 + && value.find(key_t(std::string(300, 'z')))->template get() == 4; +} + +template +bool is_missing(const J& value, const char* name) +{ + return value.find(typename J::object_t::key_type(name)) == value.end(); +} + +// member access through find(): at() does not compile for key types without +// size() or a conversion to string_t (key_to_json), as in version 3.12.0 +template +const J& member(const J& value, const char* name) +{ + const auto it = value.find(typename J::object_t::key_type(name)); + REQUIRE(it != value.end()); + return *it; +} +} // namespace custom_key_test + +TEST_CASE_TEMPLATE("custom object key types: copy", J, + custom_key_test::json_full, custom_key_test::json_no_eq, custom_key_test::json_explicit, + custom_key_test::json_to_json, custom_key_test::json_c_str) +{ + SECTION("shallow") + { + const J original = custom_key_test::make_shallow(); + REQUIRE(custom_key_test::check_shallow(original)); + + const J copy(original); // NOLINT(performance-unnecessary-copy-initialization) + CHECK(custom_key_test::check_shallow(copy)); + + J assigned; + assigned = original; + CHECK(custom_key_test::check_shallow(assigned)); + + // the original is unchanged + CHECK(custom_key_test::check_shallow(original)); + } + + SECTION("deep") + { + const std::size_t depth = custom_key_test::deep_depth(); + + const J original = custom_key_test::make_deep(depth, false); + REQUIRE(custom_key_test::check_deep(original, depth, false)); + + const J copy(original); // NOLINT(performance-unnecessary-copy-initialization) + CHECK(custom_key_test::check_deep(copy, depth, false)); + + J assigned; + assigned = original; + CHECK(custom_key_test::check_deep(assigned, depth, false)); + + CHECK(custom_key_test::check_deep(original, depth, false)); + } +} + +TEST_CASE_TEMPLATE("custom object key types: parse", J, + custom_key_test::json_full, custom_key_test::json_no_eq, custom_key_test::json_explicit, + custom_key_test::json_to_json, custom_key_test::json_c_str) +{ + const J j = J::parse(R"({"a":1,"b":{"c":[1,2]}})"); + + CHECK(j.size() == 2); + CHECK(custom_key_test::member(j, "a").template get() == 1); + CHECK(custom_key_test::member(custom_key_test::member(j, "b"), "c").size() == 2); + CHECK(custom_key_test::member(custom_key_test::member(j, "b"), "c")[1].template get() == 2); + + // a deeply nested document + const std::size_t depth = custom_key_test::deep_depth(); + std::string text; + for (std::size_t i = 0; i < depth; ++i) + { + text += "{\"k" + std::to_string(i) + "\":"; + } + text += "1"; + text.append(depth, '}'); + CHECK(custom_key_test::check_deep(J::parse(text), depth, false)); +} + +TEST_CASE_TEMPLATE("custom object key types: merge_patch, update, and insert", J, + custom_key_test::json_full, custom_key_test::json_no_eq, custom_key_test::json_explicit, + custom_key_test::json_to_json, custom_key_test::json_c_str) +{ + SECTION("merge_patch") + { + J j = J::parse(R"({"a":1,"b":2,"n":{"x":1,"y":2}})"); + j.merge_patch(J::parse(R"({"b":null,"c":3,"n":{"y":null,"z":3}})")); + + CHECK(j.size() == 3); + CHECK(custom_key_test::member(j, "a").template get() == 1); + CHECK(custom_key_test::is_missing(j, "b")); + CHECK(custom_key_test::member(j, "c").template get() == 3); + CHECK(custom_key_test::member(j, "n").size() == 2); + CHECK(custom_key_test::member(custom_key_test::member(j, "n"), "x").template get() == 1); + CHECK(custom_key_test::member(custom_key_test::member(j, "n"), "z").template get() == 3); + } + + SECTION("update") + { + J j = J::parse(R"({"a":1,"b":2,"n":{"x":1}})"); + const J other = J::parse(R"({"b":3,"c":4,"n":{"y":2}})"); + + J replaced = j; + replaced.update(other); + CHECK(replaced.size() == 4); + CHECK(custom_key_test::member(replaced, "a").template get() == 1); + CHECK(custom_key_test::member(replaced, "b").template get() == 3); + CHECK(custom_key_test::member(replaced, "c").template get() == 4); + CHECK(custom_key_test::member(replaced, "n").size() == 1); + CHECK(custom_key_test::member(custom_key_test::member(replaced, "n"), "y").template get() == 2); + + j.update(other, true); + CHECK(j.size() == 4); + CHECK(custom_key_test::member(j, "n").size() == 2); + CHECK(custom_key_test::member(custom_key_test::member(j, "n"), "x").template get() == 1); + CHECK(custom_key_test::member(custom_key_test::member(j, "n"), "y").template get() == 2); + } + + SECTION("insert") + { + J j = J::parse(R"({"a":1,"b":2})"); + const J other = J::parse(R"({"b":3,"c":4})"); + j.insert(other.begin(), other.end()); + + CHECK(j.size() == 3); + CHECK(custom_key_test::member(j, "b").template get() == 2); + CHECK(custom_key_test::member(j, "c").template get() == 4); + } +} + +TEST_CASE_TEMPLATE("custom object key types: at() reports a missing key", J, + custom_key_test::json_full, custom_key_test::json_no_eq, custom_key_test::json_explicit, + custom_key_test::json_c_str) +{ + // not for key_to_json: at() needs the key's size() or a conversion to + // string_t for its error message, which also was the case in version 3.12.0 + J j = J::parse(R"({"a":1})"); + const J& j_const = j; + + CHECK(j.at("a").template get() == 1); + CHECK(j_const.at("a").template get() == 1); + CHECK_THROWS_WITH_AS(j.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", typename J::out_of_range&); + CHECK_THROWS_WITH_AS(j_const.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", typename J::out_of_range&); +} + +TEST_CASE_TEMPLATE("custom object key types: BSON", J, + custom_key_test::json_full, custom_key_test::json_no_eq) +{ + SECTION("shallow") + { + const J value = custom_key_test::make_shallow(); + const nlohmann::json expected = custom_key_test::make_shallow(); + + const std::vector encoded = J::to_bson(value); + CHECK(encoded == nlohmann::json::to_bson(expected)); + CHECK(nlohmann::json::from_bson(encoded) == expected); + } + + SECTION("deep") + { + const std::size_t depth = custom_key_test::deep_depth(); + const J value = custom_key_test::make_deep(depth, false); + const nlohmann::json expected = custom_key_test::make_deep(depth, false); + + const std::vector encoded = J::to_bson(value); + CHECK(encoded == nlohmann::json::to_bson(expected)); + CHECK(nlohmann::json::from_bson(encoded) == expected); + } +} + +TEST_CASE_TEMPLATE("custom object key types: CBOR", J, + custom_key_test::json_full, custom_key_test::json_no_eq, custom_key_test::json_explicit, + custom_key_test::json_to_json, custom_key_test::json_c_str) +{ + SECTION("shallow") + { + const J value = custom_key_test::make_shallow(); + const nlohmann::json expected = custom_key_test::make_shallow(); + + const std::vector encoded = J::to_cbor(value); + CHECK(encoded == nlohmann::json::to_cbor(expected)); + CHECK(nlohmann::json::from_cbor(encoded) == expected); + } + + SECTION("deeper than the recursion depth limit") + { + const std::size_t depth = custom_key_test::deep_depth(); + const J value = custom_key_test::make_deep(depth, true); + const nlohmann::json expected = custom_key_test::make_deep(depth, true); + + const std::vector encoded = J::to_cbor(value); + CHECK(encoded == nlohmann::json::to_cbor(expected)); + CHECK(nlohmann::json::from_cbor(encoded) == expected); + } +} + +TEST_CASE_TEMPLATE("custom object key types: MessagePack", J, + custom_key_test::json_full, custom_key_test::json_no_eq, custom_key_test::json_explicit, + custom_key_test::json_to_json, custom_key_test::json_c_str) +{ + SECTION("shallow") + { + const J value = custom_key_test::make_shallow(); + const nlohmann::json expected = custom_key_test::make_shallow(); + + const std::vector encoded = J::to_msgpack(value); + CHECK(encoded == nlohmann::json::to_msgpack(expected)); + CHECK(nlohmann::json::from_msgpack(encoded) == expected); + } + + SECTION("deeper than the recursion depth limit") + { + const std::size_t depth = custom_key_test::deep_depth(); + const J value = custom_key_test::make_deep(depth, true); + const nlohmann::json expected = custom_key_test::make_deep(depth, true); + + const std::vector encoded = J::to_msgpack(value); + CHECK(encoded == nlohmann::json::to_msgpack(expected)); + CHECK(nlohmann::json::from_msgpack(encoded) == expected); + } +}