From eb8899a29cf078507f20e50197d4aa713937bbe5 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sun, 11 Oct 2026 08:24:31 +0200 Subject: [PATCH] Restore v3.12.0 support for custom object key types (#5795) * 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 * Use the with_object_t alias for the custom object key test types Signed-off-by: Niels Lohmann * Avoid floating-point equality in custom key type test GCC with -Werror=float-equal rejects comparing the double value with ==. Signed-off-by: Niels Lohmann * Write the head of nested BSON elements in one helper Signed-off-by: Niels Lohmann * Suppress bugprone-return-const-ref-from-parameter in key_for_message The reference is only passed to concat() within the full-expression that holds the key, like the similar helpers in binary_writer.hpp. Signed-off-by: Niels Lohmann * Make the value of the custom test key types private clang-tidy (cppcoreguidelines-non-private-member-variables-in-classes) rejects the protected member; the derived key types use a protected accessor instead. Signed-off-by: Niels Lohmann * Pass keys with data() and size() unchanged into the at() miss message key_for_message() converted every key that string_t can be constructed from, so a miss on a string_t or string_view key copied it before concat() copied it again. Keys that concat() can append through data() and size() are now passed through; only other keys (string literals, key types that just convert to string_t) are converted. Signed-off-by: Niels Lohmann --------- 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 | 79 ++- include/nlohmann/json.hpp | 39 +- tests/src/custom_key_test.hpp | 519 ++++++++++++++++++ tests/src/custom_object_key_type.hpp | 2 +- tests/src/unit-custom-key-type-c-str.cpp | 46 ++ tests/src/unit-custom-key-type-explicit.cpp | 46 ++ tests/src/unit-custom-key-type-full.cpp | 51 ++ tests/src/unit-custom-key-type-no-eq.cpp | 51 ++ tests/src/unit-custom-key-type-to-json.cpp | 41 ++ 11 files changed, 855 insertions(+), 26 deletions(-) create mode 100644 tests/src/custom_key_test.hpp create mode 100644 tests/src/unit-custom-key-type-c-str.cpp create mode 100644 tests/src/unit-custom-key-type-explicit.cpp create mode 100644 tests/src/unit-custom-key-type-full.cpp create mode 100644 tests/src/unit-custom-key-type-no-eq.cpp create mode 100644 tests/src/unit-custom-key-type-to-json.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 59a643edf..026ab9c13 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -313,12 +313,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()); @@ -326,7 +320,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; @@ -591,12 +585,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); @@ -604,7 +592,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; @@ -994,7 +982,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); @@ -1071,7 +1059,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); @@ -1676,6 +1664,21 @@ class binary_writer } } + /*! + @brief Writes the head of a BSON element with key @a name whose value is + the object or array @a j: its type, key, and @a size + + Called while the key is in scope: an object key is a key_type, which + may only convert to a temporary string_t, so it cannot be kept by + pointer until after the loop over the entries. + */ + void write_bson_nested_head(const string_t& name, const BasicJsonType& j, + const std::size_t size) + { + write_bson_entry_header(name, j.is_object() ? 0x03 : 0x04); + write_number(to_bson_length(size), true); + } + /// @brief an object or array of the BSON document being sized or written struct bson_frame { @@ -1840,7 +1843,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()) { @@ -1851,7 +1853,7 @@ class binary_writer ++current.member; if (el.second.is_structured()) { - nested_name = &el.first; + write_bson_nested_head(el.first, el.second, nested_sizes[next_size++]); nested = &el.second; } else @@ -1870,7 +1872,7 @@ class binary_writer ++current.index; if (el.is_structured()) { - nested_name = &index_name; + write_bson_nested_head(index_name, el, nested_sizes[next_size++]); nested = ⪙ } else @@ -1882,8 +1884,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; @@ -1951,6 +1951,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 b841e9fa3..b91fa3e27 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,28 @@ 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(detail::exception_id::key_not_found, detail::concat("key '", string_t(key), "' not found"), &j)); + JSON_THROW(out_of_range::create(detail::exception_id::key_not_found, 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 keys with data() and size() (such as string_t itself or a string + /// view) are passed through unchanged, so a miss does not copy them; + /// other keys (such as string literals or key types that only convert + /// to string_t) are converted to string_t + template < typename KeyType, detail::enable_if_t < detail::detect_string_can_append_data::value, int > = 0 > + static const KeyType & key_for_message(const KeyType& key) + { + return key; // NOLINT(bugprone-return-const-ref-from-parameter): the result is only passed to concat() within the full-expression that holds key + } + + template < typename KeyType, detail::enable_if_t < !detail::detect_string_can_append_data::value, int > = 0 > + static string_t key_for_message(const KeyType& key) + { + return string_t(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/custom_key_test.hpp b/tests/src/custom_key_test.hpp new file mode 100644 index 000000000..224517b0f --- /dev/null +++ b/tests/src/custom_key_test.hpp @@ -0,0 +1,519 @@ +// __ _____ _____ _____ +// __| | __| | | | 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 + +#pragma once + +#include "doctest_compatibility.h" + +#include + +#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: + const std::string& str() const noexcept + { + return m_value; + } + + private: + 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 str(); + } + + friend bool operator==(const key_full& lhs, const key_full& rhs) + { + return lhs.str() == rhs.str(); + } +}; + +// 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 str(); + } +}; + +// 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 str(); + } +}; + +// 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 str(); + } +}; + +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 str(); + } + + std::size_t size() const + { + return str().size(); + } + + const char* c_str() const + { + return str().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::json::with_object_t::type>; +using json_no_eq = nlohmann::json::with_object_t::type>; +using json_explicit = nlohmann::json::with_object_t::type>; +using json_to_json = nlohmann::json::with_object_t::type>; +using json_c_str = nlohmann::json::with_object_t::type>; + +// a key that is long enough to need a length byte in CBOR and MessagePack +inline 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 +inline 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; +} + +inline 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() && std::abs(d->template get() - 2.5) < 1e-9 + && 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; +} + +// The test cases, as function templates: each unit-custom-key-type-*.cpp file +// runs them for one object type only. The MinGW linker fails on objects with +// more than 65535 sections, and every basic_json specialization adds many +// (see .github/workflows/windows.yml). + +// copy +template +void test_copy() +{ + 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)); + } +} + +// parse +template +void test_parse() +{ + 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)); +} + +// merge_patch, update, and insert +template +void test_patch() +{ + 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); + } +} + +// at() reports a missing key +template +void test_at() +{ + // 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); +#if JSON_DIAGNOSTIC_POSITIONS + CHECK_THROWS_WITH_AS(j.at("missing"), "[json.exception.out_of_range.403] (bytes 0-7) key 'missing' not found", typename J::out_of_range&); + CHECK_THROWS_WITH_AS(j_const.at("missing"), "[json.exception.out_of_range.403] (bytes 0-7) key 'missing' not found", typename J::out_of_range&); +#else + 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&); +#endif +} + +// BSON +template +void test_bson() +{ + 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); + } +} + +// CBOR +template +void test_cbor() +{ + 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); + } +} + +// MessagePack +template +void test_msgpack() +{ + 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); + } +} + +} // namespace custom_key_test diff --git a/tests/src/custom_object_key_type.hpp b/tests/src/custom_object_key_type.hpp index fa23736d4..ebf499844 100644 --- a/tests/src/custom_object_key_type.hpp +++ b/tests/src/custom_object_key_type.hpp @@ -71,5 +71,5 @@ class object using base_type::base_type; }; -using json = nlohmann::basic_json; +using json = nlohmann::json::with_object_t; } // namespace custom_object_key_test diff --git a/tests/src/unit-custom-key-type-c-str.cpp b/tests/src/unit-custom-key-type-c-str.cpp new file mode 100644 index 000000000..5ad1edc90 --- /dev/null +++ b/tests/src/unit-custom-key-type-c-str.cpp @@ -0,0 +1,46 @@ +// __ _____ _____ _____ +// __| | __| | | | 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 "custom_key_test.hpp" + +// object type whose key type has no conversion, but size() and c_str() (see custom_key_test.hpp) +using custom_key_test::json_c_str; + +TEST_CASE("custom object key types: copy (json_c_str)") +{ + custom_key_test::test_copy(); +} + +TEST_CASE("custom object key types: parse (json_c_str)") +{ + custom_key_test::test_parse(); +} + +TEST_CASE("custom object key types: merge_patch, update, and insert (json_c_str)") +{ + custom_key_test::test_patch(); +} + +TEST_CASE("custom object key types: at() reports a missing key (json_c_str)") +{ + custom_key_test::test_at(); +} + +TEST_CASE("custom object key types: CBOR (json_c_str)") +{ + custom_key_test::test_cbor(); +} + +TEST_CASE("custom object key types: MessagePack (json_c_str)") +{ + custom_key_test::test_msgpack(); +} diff --git a/tests/src/unit-custom-key-type-explicit.cpp b/tests/src/unit-custom-key-type-explicit.cpp new file mode 100644 index 000000000..2ef3e97f1 --- /dev/null +++ b/tests/src/unit-custom-key-type-explicit.cpp @@ -0,0 +1,46 @@ +// __ _____ _____ _____ +// __| | __| | | | 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 "custom_key_test.hpp" + +// object type whose key type has explicit conversion to std::string, no operator== (see custom_key_test.hpp) +using custom_key_test::json_explicit; + +TEST_CASE("custom object key types: copy (json_explicit)") +{ + custom_key_test::test_copy(); +} + +TEST_CASE("custom object key types: parse (json_explicit)") +{ + custom_key_test::test_parse(); +} + +TEST_CASE("custom object key types: merge_patch, update, and insert (json_explicit)") +{ + custom_key_test::test_patch(); +} + +TEST_CASE("custom object key types: at() reports a missing key (json_explicit)") +{ + custom_key_test::test_at(); +} + +TEST_CASE("custom object key types: CBOR (json_explicit)") +{ + custom_key_test::test_cbor(); +} + +TEST_CASE("custom object key types: MessagePack (json_explicit)") +{ + custom_key_test::test_msgpack(); +} diff --git a/tests/src/unit-custom-key-type-full.cpp b/tests/src/unit-custom-key-type-full.cpp new file mode 100644 index 000000000..b72bd7ae0 --- /dev/null +++ b/tests/src/unit-custom-key-type-full.cpp @@ -0,0 +1,51 @@ +// __ _____ _____ _____ +// __| | __| | | | 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 "custom_key_test.hpp" + +// object type whose key type has implicit conversion to std::string and operator== (see custom_key_test.hpp) +using custom_key_test::json_full; + +TEST_CASE("custom object key types: copy (json_full)") +{ + custom_key_test::test_copy(); +} + +TEST_CASE("custom object key types: parse (json_full)") +{ + custom_key_test::test_parse(); +} + +TEST_CASE("custom object key types: merge_patch, update, and insert (json_full)") +{ + custom_key_test::test_patch(); +} + +TEST_CASE("custom object key types: at() reports a missing key (json_full)") +{ + custom_key_test::test_at(); +} + +TEST_CASE("custom object key types: BSON (json_full)") +{ + custom_key_test::test_bson(); +} + +TEST_CASE("custom object key types: CBOR (json_full)") +{ + custom_key_test::test_cbor(); +} + +TEST_CASE("custom object key types: MessagePack (json_full)") +{ + custom_key_test::test_msgpack(); +} diff --git a/tests/src/unit-custom-key-type-no-eq.cpp b/tests/src/unit-custom-key-type-no-eq.cpp new file mode 100644 index 000000000..288b9fdd2 --- /dev/null +++ b/tests/src/unit-custom-key-type-no-eq.cpp @@ -0,0 +1,51 @@ +// __ _____ _____ _____ +// __| | __| | | | 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 "custom_key_test.hpp" + +// object type whose key type has implicit conversion to std::string, no operator== (see custom_key_test.hpp) +using custom_key_test::json_no_eq; + +TEST_CASE("custom object key types: copy (json_no_eq)") +{ + custom_key_test::test_copy(); +} + +TEST_CASE("custom object key types: parse (json_no_eq)") +{ + custom_key_test::test_parse(); +} + +TEST_CASE("custom object key types: merge_patch, update, and insert (json_no_eq)") +{ + custom_key_test::test_patch(); +} + +TEST_CASE("custom object key types: at() reports a missing key (json_no_eq)") +{ + custom_key_test::test_at(); +} + +TEST_CASE("custom object key types: BSON (json_no_eq)") +{ + custom_key_test::test_bson(); +} + +TEST_CASE("custom object key types: CBOR (json_no_eq)") +{ + custom_key_test::test_cbor(); +} + +TEST_CASE("custom object key types: MessagePack (json_no_eq)") +{ + custom_key_test::test_msgpack(); +} diff --git a/tests/src/unit-custom-key-type-to-json.cpp b/tests/src/unit-custom-key-type-to-json.cpp new file mode 100644 index 000000000..fde2a8ad3 --- /dev/null +++ b/tests/src/unit-custom-key-type-to-json.cpp @@ -0,0 +1,41 @@ +// __ _____ _____ _____ +// __| | __| | | | 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 "custom_key_test.hpp" + +// object type whose key type has no conversion, only a to_json overload (see custom_key_test.hpp) +using custom_key_test::json_to_json; + +TEST_CASE("custom object key types: copy (json_to_json)") +{ + custom_key_test::test_copy(); +} + +TEST_CASE("custom object key types: parse (json_to_json)") +{ + custom_key_test::test_parse(); +} + +TEST_CASE("custom object key types: merge_patch, update, and insert (json_to_json)") +{ + custom_key_test::test_patch(); +} + +TEST_CASE("custom object key types: CBOR (json_to_json)") +{ + custom_key_test::test_cbor(); +} + +TEST_CASE("custom object key types: MessagePack (json_to_json)") +{ + custom_key_test::test_msgpack(); +}