From d540750f21899cb063b5ca1b6933cc1e49bdc2fc Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 9 Oct 2026 16:56:14 +0200 Subject: [PATCH 1/6] Use the with_*_t aliases in the remaining tests and docs (#5790) #5787 missed the instantiations spelled as `basic_json <` (astyle's formatting) or ending in the CustomBaseClass argument. Convert them, including the binary_t.md example pointed out in review. Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/binary_t.md | 13 +----- docs/mkdocs/docs/examples/as_base_class.cpp | 14 +----- tests/src/unit-bson.cpp | 8 +--- tests/src/unit-custom-base-class.cpp | 29 +----------- tests/src/unit-custom-binary-type.cpp | 8 +--- tests/src/unit-msgpack.cpp | 52 +++------------------ tests/src/unit-ordered_json2.cpp | 11 +---- 7 files changed, 15 insertions(+), 120 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/binary_t.md b/docs/mkdocs/docs/api/basic_json/binary_t.md index 46b0f7ae2..da13c22a2 100644 --- a/docs/mkdocs/docs/api/basic_json/binary_t.md +++ b/docs/mkdocs/docs/api/basic_json/binary_t.md @@ -64,18 +64,7 @@ values of that type directly to a `basic_json` instance, and they will automatic rather than arrays: ```cpp -using custom_json = nlohmann::basic_json< - nlohmann::ordered_map, // ObjectType - std::vector, // ArrayType - std::string, // StringType - bool, // BooleanType - std::int64_t, // NumberIntegerType - std::uint64_t, // NumberUnsignedType - double, // NumberFloatType - std::allocator, // AllocatorType - nlohmann::adl_serializer, - std::vector // Custom BinaryType ->; +using custom_json = nlohmann::ordered_json::with_binary_t>; std::vector data{std::byte{1}, std::byte{2}, std::byte{3}}; custom_json j = data; // Creates a binary value, not an array diff --git a/docs/mkdocs/docs/examples/as_base_class.cpp b/docs/mkdocs/docs/examples/as_base_class.cpp index 48357b045..b5b62d78f 100644 --- a/docs/mkdocs/docs/examples/as_base_class.cpp +++ b/docs/mkdocs/docs/examples/as_base_class.cpp @@ -15,19 +15,7 @@ class base_class_with_hidden_members } }; -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, - base_class_with_hidden_members - >; +using json = nlohmann::json::with_base_class_t; int main() { diff --git a/tests/src/unit-bson.cpp b/tests/src/unit-bson.cpp index ea5ca3130..03c901cee 100644 --- a/tests/src/unit-bson.cpp +++ b/tests/src/unit-bson.cpp @@ -46,9 +46,7 @@ class huge_binary_t : public std::vector } }; -using huge_binary_json = nlohmann::basic_json < - std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, - double, std::allocator, nlohmann::adl_serializer, huge_binary_t, void >; +using huge_binary_json = nlohmann::json::with_binary_t; // a string type that can be made to report a size beyond INT32_MAX without // allocating that much memory, so BSON length overflow can be tested for @@ -96,9 +94,7 @@ class huge_string_t : public std::string bool pretend_huge = false; }; -using huge_string_json = nlohmann::basic_json < - std::map, std::vector, huge_string_t, bool, std::int64_t, std::uint64_t, - double, std::allocator, nlohmann::adl_serializer, std::vector, void >; +using huge_string_json = nlohmann::json::with_string_t; } // namespace TEST_CASE("BSON") diff --git a/tests/src/unit-custom-base-class.cpp b/tests/src/unit-custom-base-class.cpp index 9718d1cda..673c7ddbb 100644 --- a/tests/src/unit-custom-base-class.cpp +++ b/tests/src/unit-custom-base-class.cpp @@ -400,20 +400,7 @@ class base_class_with_hidden_members 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, - base_class_with_hidden_members - >; +using json_with_hidden_base_members = nlohmann::json::with_base_class_t; TEST_CASE("JSON Node as_base_class") { @@ -459,19 +446,7 @@ struct const_member_base const int id = 7; // NOLINT(misc-non-private-member-variables-in-classes) }; -using json_with_const_base = nlohmann::basic_json < - std::map, - std::vector, - std::string, - bool, - std::int64_t, - std::uint64_t, - double, - std::allocator, - nlohmann::adl_serializer, - std::vector, - const_member_base - >; +using json_with_const_base = nlohmann::json::with_base_class_t; // build an array nested @a depth levels deep, with the innermost value 1; // every level is constructed (never assigned), since const_member_base does diff --git a/tests/src/unit-custom-binary-type.cpp b/tests/src/unit-custom-binary-type.cpp index d885376cb..6b504bd51 100644 --- a/tests/src/unit-custom-binary-type.cpp +++ b/tests/src/unit-custom-binary-type.cpp @@ -26,15 +26,11 @@ namespace // a BinaryType whose value type is signed: the elements must still be // processed as the numbers 0..255 -using char_binary_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, void >; +using char_binary_json = nlohmann::json::with_binary_t>; #ifdef JSON_HAS_CPP_17 // a BinaryType whose value type is not an integer type at all - using byte_binary_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, void >; + using byte_binary_json = nlohmann::json::with_binary_t>; #endif } // namespace diff --git a/tests/src/unit-msgpack.cpp b/tests/src/unit-msgpack.cpp index 0e07c8ea4..1e1005ad7 100644 --- a/tests/src/unit-msgpack.cpp +++ b/tests/src/unit-msgpack.cpp @@ -2202,10 +2202,7 @@ struct huge_array : std::vector } }; -using huge_array_json = nlohmann::basic_json < - std::map, huge_array, std::string, bool, std::int64_t, std::uint64_t, - double, std::allocator, nlohmann::adl_serializer, - std::vector, void >; +using huge_array_json = nlohmann::json::with_array_t; TEST_CASE("MessagePack Size above uint32 for array") { @@ -2250,18 +2247,7 @@ template, - void >; +using huge_object_json = nlohmann::json::with_object_t; TEST_CASE("MessagePack Size above uint32 for object") { @@ -2296,18 +2282,7 @@ struct huge_string : std::string } }; -using huge_string_json = nlohmann::basic_json < - std::map, - std::vector, - huge_string, - bool, - std::int64_t, - std::uint64_t, - double, - std::allocator, - nlohmann::adl_serializer, - std::vector, - void >; +using huge_string_json = nlohmann::json::with_string_t; TEST_CASE("MessagePack Size above uint32 for string") { @@ -2330,18 +2305,7 @@ struct huge_binary : std::vector } }; -using huge_binary_json = nlohmann::basic_json < - std::map, - std::vector, - std::string, - bool, - std::int64_t, - std::uint64_t, - double, - std::allocator, - nlohmann::adl_serializer, - huge_binary, - void >; +using huge_binary_json = nlohmann::json::with_binary_t; TEST_CASE("MessagePack Size above uint32 for binary") { @@ -2391,14 +2355,10 @@ class beyond_uint32_string_t : public std::string } }; -using beyond_uint32_string_json = nlohmann::basic_json < - std::map, std::vector, beyond_uint32_string_t, bool, std::int64_t, std::uint64_t, - double, std::allocator, nlohmann::adl_serializer, std::vector, void >; +using beyond_uint32_string_json = nlohmann::json::with_string_t; #endif -using beyond_uint32_binary_json = nlohmann::basic_json < - std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, - double, std::allocator, nlohmann::adl_serializer, beyond_uint32_binary_t, void >; +using beyond_uint32_binary_json = nlohmann::json::with_binary_t; } // namespace TEST_CASE("MessagePack lengths beyond UINT32_MAX cannot be serialized") diff --git a/tests/src/unit-ordered_json2.cpp b/tests/src/unit-ordered_json2.cpp index 653820628..4f4e18844 100644 --- a/tests/src/unit-ordered_json2.cpp +++ b/tests/src/unit-ordered_json2.cpp @@ -217,16 +217,7 @@ void int_to_string(alt_string& target, std::size_t value) target = std::to_string(value).c_str(); } -using alt_json = nlohmann::basic_json < - std::map, - std::vector, - alt_string, - bool, - std::int64_t, - std::uint64_t, - double, - std::allocator, - nlohmann::adl_serializer >; +using alt_json = nlohmann::json::with_string_t; bool operator<(const char* op1, const alt_string& op2) noexcept { From 374dfe4f0f3f259b1ccaab879a4ab706dbf97e4c Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 9 Oct 2026 17:05:36 +0200 Subject: [PATCH 2/6] Keep converted object keys alive while writing UBJSON and BJData (#5791) * Keep converted object keys alive while writing UBJSON and BJData Since #5746, write_ubjson and write_ubjson_iterative pass each object key to sanitize_utf8_for_write and keep the returned reference. When object_t::key_type is not string_t but converts to it, the argument is a temporary that is destroyed at the end of the statement, and the function returns a reference to it in every case but a sanitized copy, so the key bytes are read from a dead object (AddressSanitizer: stack-use-after-scope). Default json and ordered_json are unaffected. Bind the key to a named object_key_string_t first: a reference when key_type is string_t, so no copy is added there, and a converted copy otherwise. A deleted overload of sanitize_utf8_for_write for anything other than string_t turns a recurrence into a compile error. Signed-off-by: Niels Lohmann * Suppress -Wunused-member-function for the converting_key test type converting_key::data() is only called when JSON_DIAGNOSTICS is enabled. Signed-off-by: Niels Lohmann * Fix clang-tidy findings in the UBJSON/BJData converted-key fix Suppress hicpp/modernize-use-equals-delete on the deleted sanitize_utf8_for_write overload: it guards a private helper and must stay private. Replace the C-style array in the new test with std::array. Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- .../nlohmann/detail/output/binary_writer.hpp | 23 ++- single_include/nlohmann/json.hpp | 23 ++- tests/src/unit-binary_utf8_error_handler.cpp | 157 ++++++++++++++++++ 3 files changed, 199 insertions(+), 4 deletions(-) diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index 4af3ff9fb..212d2658f 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -85,6 +85,12 @@ template::value, + const string_t&, string_t >::type; using binary_t = typename BasicJsonType::binary_t; using number_float_t = typename BasicJsonType::number_float_t; @@ -776,8 +782,10 @@ class binary_writer for (const auto& el : *j.m_data.m_value.object) { + // a converted key must outlive the reference returned by sanitize_utf8_for_write + const object_key_string_t key_string = el.first; string_t storage; - const string_t& key = sanitize_utf8_for_write(el.first, j, storage); + const string_t& key = sanitize_utf8_for_write(key_string, j, storage); write_number_with_ubjson_prefix(key.size(), true, use_bjdata); oa.write_characters( reinterpret_cast(key.data()), @@ -1317,8 +1325,10 @@ class binary_writer continue; } + // a converted key must outlive the reference returned by sanitize_utf8_for_write + const object_key_string_t key_string = current.object_it->first; string_t storage; - const string_t& key = sanitize_utf8_for_write(current.object_it->first, j, storage); + const string_t& key = sanitize_utf8_for_write(key_string, j, storage); write_number_with_ubjson_prefix(key.size(), true, use_bjdata); oa.write_characters( reinterpret_cast(key.data()), @@ -2760,6 +2770,11 @@ class binary_writer itself in every case but a sanitized `replace`/`ignore` one, so @a storage must outlive the returned reference only then. + @a s must be an lvalue that outlives the returned reference. An object key + whose `key_type` is not @ref string_t must therefore first be converted + into a named string_t (see @ref object_key_string_t); the deleted overload + below enforces this at compile time. + @param[in] s the string (value or object key) to write @param[in] context the value @a s belongs to (for diagnostics) @param[out] storage backing storage for a sanitized copy @@ -2789,6 +2804,10 @@ class binary_writer } } + /// deleted: anything but a string_t would bind a temporary that dies before the returned reference is used + template < typename T, enable_if_t < !std::is_same::value, int > = 0 > + const string_t& sanitize_utf8_for_write(const T& /*s*/, const BasicJsonType& /*context*/, string_t& /*storage*/) const = delete; // NOLINT(hicpp-use-equals-delete,modernize-use-equals-delete): a private helper's guard, not part of the interface + /*! @brief write an integer in the shortest encoding diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 428a6abbf..2e6838bbc 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -21590,6 +21590,12 @@ template::value, + const string_t&, string_t >::type; using binary_t = typename BasicJsonType::binary_t; using number_float_t = typename BasicJsonType::number_float_t; @@ -22281,8 +22287,10 @@ class binary_writer for (const auto& el : *j.m_data.m_value.object) { + // a converted key must outlive the reference returned by sanitize_utf8_for_write + const object_key_string_t key_string = el.first; string_t storage; - const string_t& key = sanitize_utf8_for_write(el.first, j, storage); + const string_t& key = sanitize_utf8_for_write(key_string, j, storage); write_number_with_ubjson_prefix(key.size(), true, use_bjdata); oa.write_characters( reinterpret_cast(key.data()), @@ -22822,8 +22830,10 @@ class binary_writer continue; } + // a converted key must outlive the reference returned by sanitize_utf8_for_write + const object_key_string_t key_string = current.object_it->first; string_t storage; - const string_t& key = sanitize_utf8_for_write(current.object_it->first, j, storage); + const string_t& key = sanitize_utf8_for_write(key_string, j, storage); write_number_with_ubjson_prefix(key.size(), true, use_bjdata); oa.write_characters( reinterpret_cast(key.data()), @@ -24265,6 +24275,11 @@ class binary_writer itself in every case but a sanitized `replace`/`ignore` one, so @a storage must outlive the returned reference only then. + @a s must be an lvalue that outlives the returned reference. An object key + whose `key_type` is not @ref string_t must therefore first be converted + into a named string_t (see @ref object_key_string_t); the deleted overload + below enforces this at compile time. + @param[in] s the string (value or object key) to write @param[in] context the value @a s belongs to (for diagnostics) @param[out] storage backing storage for a sanitized copy @@ -24294,6 +24309,10 @@ class binary_writer } } + /// deleted: anything but a string_t would bind a temporary that dies before the returned reference is used + template < typename T, enable_if_t < !std::is_same::value, int > = 0 > + const string_t& sanitize_utf8_for_write(const T& /*s*/, const BasicJsonType& /*context*/, string_t& /*storage*/) const = delete; // NOLINT(hicpp-use-equals-delete,modernize-use-equals-delete): a private helper's guard, not part of the interface + /*! @brief write an integer in the shortest encoding diff --git a/tests/src/unit-binary_utf8_error_handler.cpp b/tests/src/unit-binary_utf8_error_handler.cpp index c3f2435fd..114c92885 100644 --- a/tests/src/unit-binary_utf8_error_handler.cpp +++ b/tests/src/unit-binary_utf8_error_handler.cpp @@ -12,7 +12,11 @@ #include using nlohmann::json; +#include +#include +#include #include +#include #include namespace @@ -55,6 +59,53 @@ std::string dump_and_parse(const std::string& raw, eh error_handler) return json::parse(json(raw).dump(-1, ' ', false, error_handler)).get(); } +// an object key type that is not string_t, but converts implicitly to it; +// data() is only used when JSON_DIAGNOSTICS is enabled +DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-member-function") +class converting_key +{ + public: + converting_key(const char* s) : m_value(s) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) + converting_key(std::string s) : m_value(std::move(s)) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) + + // the conversion yields a temporary string_t + operator std::string() const // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) + { + return m_value; + } + + // read by the exception messages when JSON_DIAGNOSTICS is enabled + const char* data() const noexcept + { + return m_value.data(); + } + + friend bool operator<(const converting_key& lhs, const converting_key& rhs) + { + return lhs.m_value < rhs.m_value; + } + + private: + std::string m_value; +}; +DOCTEST_CLANG_SUPPRESS_WARNING_POP + +// ObjectType using converting_key; the Key template argument is ignored +template +class converting_key_object : public std::map, // NOLINT(modernize-use-transparent-functors) + typename std::allocator_traits::template rebind_alloc>> +{ + using base_type = std::map, // NOLINT(modernize-use-transparent-functors) + typename std::allocator_traits::template rebind_alloc>>; + + public: + using base_type::base_type; + using base_type::operator=; +}; + +using converting_key_json = nlohmann::basic_json; + } // namespace TEST_CASE("UTF-8 error_handler for the binary readers and writers") @@ -370,3 +421,109 @@ TEST_CASE("UTF-8 error_handler for the binary readers and writers") CHECK(json::from_bson(bson_bytes)["k"].get() == ill_formed_cases()[0].bytes); } } + +// The UBJSON and BJData writers bind the (possibly sanitized) key to a const +// string_t&. If key_type is not string_t but converts to it, the converted +// temporary must outlive that reference; this was a use-after-scope found by +// AddressSanitizer. Keys exceed the small string optimization on purpose. +TEST_CASE("UBJSON and BJData writers with an object_t whose key_type is not string_t") +{ + const std::string long_prefix(70, 'k'); + + SECTION("well-formed keys, every error_handler") + { + const std::string key1 = long_prefix + "-first"; + const std::string key2 = long_prefix + "-second"; + + converting_key_json::object_t o; + o.emplace(converting_key(key1), 1); + o.emplace(converting_key(key2), "value"); + const converting_key_json v(std::move(o)); + + json expected; + expected[key1] = 1; + expected[key2] = "value"; + + const std::array, 3> combos = {{{false, false}, {true, false}, {true, true}}}; + for (const auto h : all_handlers()) + { + CAPTURE(static_cast(h)) + for (const auto& combo : combos) + { + const bool use_count = combo.first; + const bool use_type = combo.second; + CAPTURE(use_count) + CAPTURE(use_type) + + CHECK(json::from_ubjson(converting_key_json::to_ubjson(v, use_count, use_type, h)) == expected); + CHECK(json::from_bjdata(converting_key_json::to_bjdata(v, use_count, use_type, json::bjdata_version_t::draft2, h)) == expected); + CHECK(json::from_bjdata(converting_key_json::to_bjdata(v, use_count, use_type, json::bjdata_version_t::draft3, h)) == expected); + } + } + } + + SECTION("ill-formed keys") + { + for (const auto& c : ill_formed_cases()) + { + CAPTURE(c.name) + const std::string key = long_prefix + c.bytes; + + converting_key_json::object_t o; + o.emplace(converting_key(key), 1); + const converting_key_json v(std::move(o)); + + CHECK_THROWS_AS(converting_key_json::to_ubjson(v, false, false, eh::strict), converting_key_json::type_error&); + CHECK_THROWS_AS(converting_key_json::to_bjdata(v, false, false, json::bjdata_version_t::draft2, eh::strict), converting_key_json::type_error&); + + for (const auto h : + { + eh::replace, eh::ignore + }) + { + CAPTURE(static_cast(h)) + const std::string expected = dump_and_parse(key, h); + + CHECK(json::from_ubjson(converting_key_json::to_ubjson(v, false, false, h)).begin().key() == expected); + CHECK(json::from_bjdata(converting_key_json::to_bjdata(v, false, false, json::bjdata_version_t::draft2, h)).begin().key() == expected); + } + + CHECK(json::from_ubjson(converting_key_json::to_ubjson(v, false, false, eh::keep)).begin().key() == key); + CHECK(json::from_bjdata(converting_key_json::to_bjdata(v, false, false, json::bjdata_version_t::draft2, eh::keep)).begin().key() == key); + } + } + + SECTION("nested deeper than the recursion limit") + { + // wrap the previous value, innermost first + converting_key_json v = 42; + json expected = 42; + for (int i = 199; i >= 0; --i) + { + const std::string key = "level-" + std::to_string(i) + "-" + std::string(64, 'x'); + + converting_key_json::object_t o; + o.emplace(converting_key(key), std::move(v)); + v = converting_key_json(std::move(o)); + + json e; + e[key] = std::move(expected); + expected = std::move(e); + } + + for (const auto h : all_handlers()) + { + CAPTURE(static_cast(h)) + for (const bool use_count : + { + false, true + }) + { + CAPTURE(use_count) + + CHECK(json::from_ubjson(converting_key_json::to_ubjson(v, use_count, false, h)) == expected); + CHECK(json::from_bjdata(converting_key_json::to_bjdata(v, use_count, false, json::bjdata_version_t::draft2, h)) == expected); + } + } + } +} From 44e8597701aaed2e7ff23312abeee497d203950b Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 9 Oct 2026 17:06:41 +0200 Subject: [PATCH 3/6] Flatten deeply nested values without recursing per nesting level (#5792) * Flatten deeply nested values without recursing per nesting level json_pointer::flatten() called itself once per nesting level, so flatten() on a value nested deeply enough exhausted the call stack. #5547 and #5548 fixed merge_patch() and diff() from #5393, but flatten() was left out. flatten() now walks the value with an explicit stack and keeps the path in one buffer that grows and shrinks with it. It has a single code path and no depth limit: the old version built a new path string per child, so the iterative one is no slower on shallow values and much faster on deep ones. The output, including the order of an ordered_json result, is unchanged. Signed-off-by: Niels Lohmann * Construct flatten frames in place Give the frame a constructor so both call sites can use emplace_back, as suggested in the review; index starts at 0 for every frame. Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- include/nlohmann/detail/json_pointer.hpp | 163 ++++++++++++++++------- single_include/nlohmann/json.hpp | 163 ++++++++++++++++------- tests/src/unit-json_pointer.cpp | 78 +++++++++++ 3 files changed, 308 insertions(+), 96 deletions(-) diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index 92f52aedc..0a1c0f43f 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -878,64 +878,131 @@ class json_pointer @param[in,out] result the result object to insert values to @note Empty objects or arrays are flattened to `null`. + + The value is walked with an explicit stack rather than the call stack, so + arbitrarily deeply nested values can be flattened. + + @sa https://github.com/nlohmann/json/issues/5393 */ template static void flatten(const string_t& reference_string, const BasicJsonType& value, BasicJsonType& result) { - switch (value.type()) + using object_const_iterator = typename BasicJsonType::object_t::const_iterator; + + // an array or object being walked: the container, the array index or + // object iterator of the next child, and the length of the path of the + // container itself + struct frame { - case detail::value_t::array: - { - if (value.m_data.m_value.array->empty()) - { - // flatten empty array as null - result[reference_string] = nullptr; - } - else - { - // iterate array and use index as a reference string - for (std::size_t i = 0; i < value.m_data.m_value.array->size(); ++i) - { - flatten(detail::concat(reference_string, '/', std::to_string(i)), - value.m_data.m_value.array->operator[](i), result); - } - } - break; - } + frame(const BasicJsonType* container_, object_const_iterator member_, const std::size_t path_length_) noexcept + : container(container_), member(std::move(member_)), path_length(path_length_) + {} - case detail::value_t::object: - { - if (value.m_data.m_value.object->empty()) - { - // flatten empty object as null - result[reference_string] = nullptr; - } - else - { - // iterate object and use keys as reference string - for (const auto& element : *value.m_data.m_value.object) - { - flatten(detail::concat(reference_string, '/', detail::escape(element.first)), element.second, result); - } - } - break; - } + const BasicJsonType* container; + std::size_t index = 0; + object_const_iterator member; + std::size_t path_length; + }; - case detail::value_t::null: - case detail::value_t::string: - case detail::value_t::boolean: - case detail::value_t::number_integer: - case detail::value_t::number_unsigned: - case detail::value_t::number_float: - case detail::value_t::binary: - case detail::value_t::discarded: - default: + // The containers being flattened are kept on an explicit stack, and + // every child is flattened completely before the next one, so the + // entries come out in the same order as with a recursive walk. The + // path of the value being flattened is kept in one buffer that grows + // and shrinks with the stack, rather than in a new string per level. + std::vector stack; + string_t path = reference_string; + + // flatten `v`, whose path is `path`: primitives and empty containers + // are added to the result right away; other containers get a frame + const auto enter = [&stack, &path, &result](const BasicJsonType & v) + { + switch (v.type()) { - // add a primitive value with its reference string - result[reference_string] = value; - break; + case detail::value_t::array: + { + if (v.m_data.m_value.array->empty()) + { + // flatten empty array as null + result[path] = nullptr; + } + else + { + stack.emplace_back(&v, object_const_iterator(), path.size()); + } + return; + } + + case detail::value_t::object: + { + if (v.m_data.m_value.object->empty()) + { + // flatten empty object as null + result[path] = nullptr; + } + else + { + stack.emplace_back(&v, v.m_data.m_value.object->begin(), path.size()); + } + return; + } + + case detail::value_t::null: + case detail::value_t::string: + case detail::value_t::boolean: + case detail::value_t::number_integer: + case detail::value_t::number_unsigned: + case detail::value_t::number_float: + case detail::value_t::binary: + case detail::value_t::discarded: + default: + { + // add a primitive value with its reference string + result[path] = v; + return; + } + } + }; + + enter(value); + while (!stack.empty()) + { + // the frame is changed through stack.back(): enter() may push a + // frame, which would invalidate a reference to it + const BasicJsonType* const container = stack.back().container; + + // drop the path of the previous child + path.resize(stack.back().path_length); + + if (container->is_array()) + { + const auto& array = *container->m_data.m_value.array; + const std::size_t i = stack.back().index; + if (i == array.size()) + { + stack.pop_back(); + continue; + } + + // iterate array and use index as a reference string + ++stack.back().index; + detail::concat_into(path, '/', detail::to_string(i)); + enter(array[i]); + } + else + { + const object_const_iterator it = stack.back().member; + if (it == container->m_data.m_value.object->end()) + { + stack.pop_back(); + continue; + } + + // iterate object and use keys as reference string + ++stack.back().member; + detail::concat_into(path, '/', detail::escape(it->first)); + enter(it->second); } } } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 2e6838bbc..f0b1077f5 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -20796,64 +20796,131 @@ class json_pointer @param[in,out] result the result object to insert values to @note Empty objects or arrays are flattened to `null`. + + The value is walked with an explicit stack rather than the call stack, so + arbitrarily deeply nested values can be flattened. + + @sa https://github.com/nlohmann/json/issues/5393 */ template static void flatten(const string_t& reference_string, const BasicJsonType& value, BasicJsonType& result) { - switch (value.type()) + using object_const_iterator = typename BasicJsonType::object_t::const_iterator; + + // an array or object being walked: the container, the array index or + // object iterator of the next child, and the length of the path of the + // container itself + struct frame { - case detail::value_t::array: - { - if (value.m_data.m_value.array->empty()) - { - // flatten empty array as null - result[reference_string] = nullptr; - } - else - { - // iterate array and use index as a reference string - for (std::size_t i = 0; i < value.m_data.m_value.array->size(); ++i) - { - flatten(detail::concat(reference_string, '/', std::to_string(i)), - value.m_data.m_value.array->operator[](i), result); - } - } - break; - } + frame(const BasicJsonType* container_, object_const_iterator member_, const std::size_t path_length_) noexcept + : container(container_), member(std::move(member_)), path_length(path_length_) + {} - case detail::value_t::object: - { - if (value.m_data.m_value.object->empty()) - { - // flatten empty object as null - result[reference_string] = nullptr; - } - else - { - // iterate object and use keys as reference string - for (const auto& element : *value.m_data.m_value.object) - { - flatten(detail::concat(reference_string, '/', detail::escape(element.first)), element.second, result); - } - } - break; - } + const BasicJsonType* container; + std::size_t index = 0; + object_const_iterator member; + std::size_t path_length; + }; - case detail::value_t::null: - case detail::value_t::string: - case detail::value_t::boolean: - case detail::value_t::number_integer: - case detail::value_t::number_unsigned: - case detail::value_t::number_float: - case detail::value_t::binary: - case detail::value_t::discarded: - default: + // The containers being flattened are kept on an explicit stack, and + // every child is flattened completely before the next one, so the + // entries come out in the same order as with a recursive walk. The + // path of the value being flattened is kept in one buffer that grows + // and shrinks with the stack, rather than in a new string per level. + std::vector stack; + string_t path = reference_string; + + // flatten `v`, whose path is `path`: primitives and empty containers + // are added to the result right away; other containers get a frame + const auto enter = [&stack, &path, &result](const BasicJsonType & v) + { + switch (v.type()) { - // add a primitive value with its reference string - result[reference_string] = value; - break; + case detail::value_t::array: + { + if (v.m_data.m_value.array->empty()) + { + // flatten empty array as null + result[path] = nullptr; + } + else + { + stack.emplace_back(&v, object_const_iterator(), path.size()); + } + return; + } + + case detail::value_t::object: + { + if (v.m_data.m_value.object->empty()) + { + // flatten empty object as null + result[path] = nullptr; + } + else + { + stack.emplace_back(&v, v.m_data.m_value.object->begin(), path.size()); + } + return; + } + + case detail::value_t::null: + case detail::value_t::string: + case detail::value_t::boolean: + case detail::value_t::number_integer: + case detail::value_t::number_unsigned: + case detail::value_t::number_float: + case detail::value_t::binary: + case detail::value_t::discarded: + default: + { + // add a primitive value with its reference string + result[path] = v; + return; + } + } + }; + + enter(value); + while (!stack.empty()) + { + // the frame is changed through stack.back(): enter() may push a + // frame, which would invalidate a reference to it + const BasicJsonType* const container = stack.back().container; + + // drop the path of the previous child + path.resize(stack.back().path_length); + + if (container->is_array()) + { + const auto& array = *container->m_data.m_value.array; + const std::size_t i = stack.back().index; + if (i == array.size()) + { + stack.pop_back(); + continue; + } + + // iterate array and use index as a reference string + ++stack.back().index; + detail::concat_into(path, '/', detail::to_string(i)); + enter(array[i]); + } + else + { + const object_const_iterator it = stack.back().member; + if (it == container->m_data.m_value.object->end()) + { + stack.pop_back(); + continue; + } + + // iterate object and use keys as reference string + ++stack.back().member; + detail::concat_into(path, '/', detail::escape(it->first)); + enter(it->second); } } } diff --git a/tests/src/unit-json_pointer.cpp b/tests/src/unit-json_pointer.cpp index b4ed2c9cc..20f2331e3 100644 --- a/tests/src/unit-json_pointer.cpp +++ b/tests/src/unit-json_pointer.cpp @@ -939,3 +939,81 @@ TEST_CASE("unescaping keeps a '~' that does not start an escape sequence") nlohmann::detail::unescape(s); CHECK(s == "~/~"); } + +TEST_CASE("flatten of structured values") +{ + SECTION("values nested too deeply for the call stack (#5393)") + { + // flatten() used to recurse once per nesting level + const std::size_t depth = 100000; + for (const bool objects : + { + false, true + }) + { + CAPTURE(objects) + std::string text; + std::string path; + for (std::size_t i = 0; i < depth; ++i) + { + text += objects ? "{\"a\":" : "["; + path += objects ? "/a" : "/0"; + } + text += "0"; + text += std::string(depth, objects ? '}' : ']'); + const auto value = json::parse(text); + + const auto flat = value.flatten(); + REQUIRE(flat.size() == 1); + REQUIRE(flat.begin().key().size() == path.size()); + CHECK(flat.begin().key() == path); + CHECK(flat.begin().value() == 0); + + // unflatten() is not iterative: it takes time and memory + // quadratic in the depth, so it is only roundtripped for a + // moderate depth + std::string small_text; + for (std::size_t i = 0; i < 500; ++i) + { + small_text += objects ? "{\"a\":" : "["; + } + small_text += "0"; + small_text += std::string(500, objects ? '}' : ']'); + const auto small_value = json::parse(small_text); + CHECK(small_value.flatten().unflatten() == small_value); + } + } + + SECTION("objects and arrays interleaved") + { + const json value = + { + {"a", {1, {{"b", json::array()}, {"c", json::object()}}, json::array({{{"x~/", {true, nullptr}}}})}}, + {"a/b", {{"~", 1}}}, + {"z", "s"} + }; + + const json expected = + { + {"/a/0", 1}, + {"/a/1/b", nullptr}, + {"/a/1/c", nullptr}, + {"/a/2/0/x~0~1/0", true}, + {"/a/2/0/x~0~1/1", nullptr}, + {"/a~1b/~0", 1}, + {"/z", "s"} + }; + + CHECK(value.flatten() == expected); + } + + SECTION("order of the entries of an ordered_json") + { + const auto value = nlohmann::ordered_json::parse( + R"({"z":"s","a/b":{"~":1,"k":[]},"a":[1,{"c":{},"b":[]},[{"x~/":[true,null],"w":2}]]})"); + + const auto flat = value.flatten(); + CHECK(flat.dump() == + R"({"/z":"s","/a~1b/~0":1,"/a~1b/k":null,"/a/0":1,"/a/1/c":null,"/a/1/b":null,"/a/2/0/x~0~1/0":true,"/a/2/0/x~0~1/1":null,"/a/2/0/w":2})"); + } +} From 2913e9643383b8078b9f6d5572401081047ea28e Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 9 Oct 2026 17:57:31 +0200 Subject: [PATCH 4/6] Unflatten in time and memory linear in the pointer depth (#5793) * Unflatten in time and memory linear in the pointer depth #5443 made unflatten() decide between arrays and objects independently of the iteration order by collecting the pointer prefixes that have a reference token 0 below them in a std::set>. Every such prefix was stored as a copy of all its reference tokens, and get_and_create() compared whole prefix vectors at every step, so unflattening a pointer of depth d took time and memory quadratic in d: a 10,000-level array pointer took 18 s and 1.3 GB, a 100,000-level one did not finish. The prefixes are now numbered nodes of a tree, so each is stored once and get_and_create() follows the tree token by token. The result is unchanged, including its independence of the iteration order. Signed-off-by: Niels Lohmann * Initialize prefix_tree members to satisfy -Weffc++ Signed-off-by: Niels Lohmann * Move prefix_tree setup and child insertion into member functions The constructor now creates the root node, add_child() inserts a reference token below a prefix and returns the child's number, and find_child() looks one up for get_and_create(). Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- include/nlohmann/detail/json_pointer.hpp | 83 ++++++++++++++++++------ single_include/nlohmann/json.hpp | 83 ++++++++++++++++++------ tests/src/unit-json_pointer.cpp | 57 ++++++++++++---- 3 files changed, 173 insertions(+), 50 deletions(-) diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index 0a1c0f43f..2ec4e3b58 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -16,8 +16,8 @@ #include // ostream #endif // JSON_NO_IO #include // max +#include // map #include // accumulate -#include // set #include // string #include // move #include // vector @@ -359,33 +359,82 @@ class json_pointer private: /*! - @brief the reference token sequences that denote arrays + @brief the pointer prefixes of a flattened object, and which of them denote arrays @ref unflatten collects the pointer prefixes that have a reference token 0 among their children; @ref get_and_create creates arrays exactly below those prefixes and objects everywhere else. Deciding this up front keeps the result independent of the order in which the flattened object is iterated, which is unspecified for some object types. + + The prefixes form a tree and are numbered, so each of them is stored only + once (as a node) rather than as a copy of all of its reference tokens. */ - using array_parents_t = std::set>; + struct prefix_tree + { + // children[id] maps a reference token to the number of the prefix + // extended by that token; number 0 is the empty prefix + std::vector> children; + // is_array[id] is true iff some flattened key has the reference token + // 0 directly below the prefix with number id + std::vector is_array; + + // start with the empty prefix only + prefix_tree() + : children(1) + , is_array(1, false) + {} + + // return the number of the prefix with number id extended by + // reference_token, adding it if it is new + std::size_t add_child(std::size_t id, string_t&& reference_token) + { + if (reference_token == "0") + { + is_array[id] = true; + } + + // read the number before the emplace_back below, which may + // reallocate children and invalidate the iterator + const std::size_t next = children.size(); + const auto inserted = children[id].emplace(std::move(reference_token), next); + const std::size_t child = inserted.first->second; + if (inserted.second) + { + children.emplace_back(); + is_array.push_back(false); + } + return child; + } + + // return the number of the prefix with number id extended by + // reference_token, which must have been added before + std::size_t find_child(std::size_t id, const string_t& reference_token) const + { + const auto it = children[id].find(reference_token); + JSON_ASSERT(it != children[id].end()); + return it->second; + } + }; /*! @brief create and return a reference to the pointed to value - Complexity: Linear in the number of reference tokens. + Complexity: Linear in the number of reference tokens (times the logarithm + of the number of siblings for the prefix lookup). @throw parse_error.106 if an array index begins with '0' @throw parse_error.109 if array index is not a number @throw type_error.313 if value cannot be unflattened */ template - BasicJsonType& get_and_create(BasicJsonType& j, const array_parents_t& array_parents) const + BasicJsonType& get_and_create(BasicJsonType& j, const prefix_tree& tree) const { auto* result = &j; - // the reference tokens that have been consumed so far; used to look up - // whether the value to be created below is an array or an object - std::vector prefix; + // the number of the prefix consumed so far; used to look up whether + // the value to be created below is an array or an object + std::size_t id = 0; // in case no reference tokens exist, return a reference to the JSON value // j which will be overwritten by a primitive value @@ -395,7 +444,7 @@ class json_pointer { case detail::value_t::null: { - if (array_parents.find(prefix) != array_parents.end()) + if (tree.is_array[id]) { // some reference token below this position is 0, so the // value is an array @@ -440,7 +489,7 @@ class json_pointer JSON_THROW(detail::type_error::create(313, "invalid value to unflatten", &j)); } - prefix.push_back(reference_token); + id = tree.find_child(id, reference_token); } return *result; @@ -1030,19 +1079,15 @@ class json_pointer // collect the pointer prefixes that have a reference token 0 among // their children; the values below them are arrays, all others are - // objects (see array_parents_t) - array_parents_t array_parents; + // objects (see prefix_tree) + prefix_tree tree; for (const auto& element : *value.m_data.m_value.object) { json_pointer ptr(element.first); - std::vector prefix; + std::size_t id = 0; for (auto& reference_token : ptr.reference_tokens) { - if (reference_token == "0") - { - array_parents.insert(prefix); - } - prefix.push_back(std::move(reference_token)); + id = tree.add_child(id, std::move(reference_token)); } } @@ -1058,7 +1103,7 @@ class json_pointer // that if the JSON pointer is "" (i.e., points to the whole value), // function get_and_create returns a reference to the result itself. // An assignment will then create a primitive value. - json_pointer(element.first).get_and_create(result, array_parents) = element.second; + json_pointer(element.first).get_and_create(result, tree) = element.second; } return result; diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index f0b1077f5..9de8ff2b2 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -19928,8 +19928,8 @@ NLOHMANN_JSON_NAMESPACE_END #include // ostream #endif // JSON_NO_IO #include // max +#include // map #include // accumulate -#include // set #include // string #include // move #include // vector @@ -20277,33 +20277,82 @@ class json_pointer private: /*! - @brief the reference token sequences that denote arrays + @brief the pointer prefixes of a flattened object, and which of them denote arrays @ref unflatten collects the pointer prefixes that have a reference token 0 among their children; @ref get_and_create creates arrays exactly below those prefixes and objects everywhere else. Deciding this up front keeps the result independent of the order in which the flattened object is iterated, which is unspecified for some object types. + + The prefixes form a tree and are numbered, so each of them is stored only + once (as a node) rather than as a copy of all of its reference tokens. */ - using array_parents_t = std::set>; + struct prefix_tree + { + // children[id] maps a reference token to the number of the prefix + // extended by that token; number 0 is the empty prefix + std::vector> children; + // is_array[id] is true iff some flattened key has the reference token + // 0 directly below the prefix with number id + std::vector is_array; + + // start with the empty prefix only + prefix_tree() + : children(1) + , is_array(1, false) + {} + + // return the number of the prefix with number id extended by + // reference_token, adding it if it is new + std::size_t add_child(std::size_t id, string_t&& reference_token) + { + if (reference_token == "0") + { + is_array[id] = true; + } + + // read the number before the emplace_back below, which may + // reallocate children and invalidate the iterator + const std::size_t next = children.size(); + const auto inserted = children[id].emplace(std::move(reference_token), next); + const std::size_t child = inserted.first->second; + if (inserted.second) + { + children.emplace_back(); + is_array.push_back(false); + } + return child; + } + + // return the number of the prefix with number id extended by + // reference_token, which must have been added before + std::size_t find_child(std::size_t id, const string_t& reference_token) const + { + const auto it = children[id].find(reference_token); + JSON_ASSERT(it != children[id].end()); + return it->second; + } + }; /*! @brief create and return a reference to the pointed to value - Complexity: Linear in the number of reference tokens. + Complexity: Linear in the number of reference tokens (times the logarithm + of the number of siblings for the prefix lookup). @throw parse_error.106 if an array index begins with '0' @throw parse_error.109 if array index is not a number @throw type_error.313 if value cannot be unflattened */ template - BasicJsonType& get_and_create(BasicJsonType& j, const array_parents_t& array_parents) const + BasicJsonType& get_and_create(BasicJsonType& j, const prefix_tree& tree) const { auto* result = &j; - // the reference tokens that have been consumed so far; used to look up - // whether the value to be created below is an array or an object - std::vector prefix; + // the number of the prefix consumed so far; used to look up whether + // the value to be created below is an array or an object + std::size_t id = 0; // in case no reference tokens exist, return a reference to the JSON value // j which will be overwritten by a primitive value @@ -20313,7 +20362,7 @@ class json_pointer { case detail::value_t::null: { - if (array_parents.find(prefix) != array_parents.end()) + if (tree.is_array[id]) { // some reference token below this position is 0, so the // value is an array @@ -20358,7 +20407,7 @@ class json_pointer JSON_THROW(detail::type_error::create(313, "invalid value to unflatten", &j)); } - prefix.push_back(reference_token); + id = tree.find_child(id, reference_token); } return *result; @@ -20948,19 +20997,15 @@ class json_pointer // collect the pointer prefixes that have a reference token 0 among // their children; the values below them are arrays, all others are - // objects (see array_parents_t) - array_parents_t array_parents; + // objects (see prefix_tree) + prefix_tree tree; for (const auto& element : *value.m_data.m_value.object) { json_pointer ptr(element.first); - std::vector prefix; + std::size_t id = 0; for (auto& reference_token : ptr.reference_tokens) { - if (reference_token == "0") - { - array_parents.insert(prefix); - } - prefix.push_back(std::move(reference_token)); + id = tree.add_child(id, std::move(reference_token)); } } @@ -20976,7 +21021,7 @@ class json_pointer // that if the JSON pointer is "" (i.e., points to the whole value), // function get_and_create returns a reference to the result itself. // An assignment will then create a primitive value. - json_pointer(element.first).get_and_create(result, array_parents) = element.second; + json_pointer(element.first).get_and_create(result, tree) = element.second; } return result; diff --git a/tests/src/unit-json_pointer.cpp b/tests/src/unit-json_pointer.cpp index 20f2331e3..1faf511f4 100644 --- a/tests/src/unit-json_pointer.cpp +++ b/tests/src/unit-json_pointer.cpp @@ -969,21 +969,54 @@ TEST_CASE("flatten of structured values") CHECK(flat.begin().key() == path); CHECK(flat.begin().value() == 0); - // unflatten() is not iterative: it takes time and memory - // quadratic in the depth, so it is only roundtripped for a - // moderate depth - std::string small_text; - for (std::size_t i = 0; i < 500; ++i) - { - small_text += objects ? "{\"a\":" : "["; - } - small_text += "0"; - small_text += std::string(500, objects ? '}' : ']'); - const auto small_value = json::parse(small_text); - CHECK(small_value.flatten().unflatten() == small_value); + // unflatten() is linear in the depth, so the value roundtrips + CHECK(flat.unflatten() == value); } } + SECTION("unflatten of a deeply nested pointer") + { + const std::size_t depth = 100000; + for (const bool objects : + { + false, true + }) + { + CAPTURE(objects) + std::string path; + for (std::size_t i = 0; i < depth; ++i) + { + path += objects ? "/a" : "/0"; + } + + json flat = json::object(); + flat[path] = 1; + const json value = flat.unflatten(); + + // walk down iteratively + std::size_t levels = 0; + const json* current = &value; + while (objects ? current->is_object() : current->is_array()) + { + REQUIRE(current->size() == 1); + current = objects ? ¤t->at("a") : ¤t->at(0); + ++levels; + } + CHECK(levels == depth); + CHECK(*current == 1); + } + } + + SECTION("unflatten does not depend on the iteration order") + { + // the "0" key comes after its sibling in iteration order + const nlohmann::ordered_json flat_array = nlohmann::ordered_json::parse(R"({"/a/1": 2, "/a/0": 1})"); + CHECK(flat_array.unflatten() == nlohmann::ordered_json::parse(R"({"a": [1, 2]})")); + + const nlohmann::ordered_json flat_object = nlohmann::ordered_json::parse(R"({"/b/1": 2})"); + CHECK(flat_object.unflatten() == nlohmann::ordered_json::parse(R"({"b": {"1": 2}})")); + } + SECTION("objects and arrays interleaved") { const json value = From 84ee12a67cbee9b10109f6b3e4a834922f5e0ae5 Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Fri, 9 Oct 2026 23:38:42 +0200 Subject: [PATCH 5/6] Make UTF8_ACCEPT/UTF8_REJECT inline variables to avoid TU-local exposure in modules (#5797) Signed-off-by: bvandijk Co-authored-by: bvandijk --- include/nlohmann/detail/string_utils.hpp | 4 ++-- single_include/nlohmann/json.hpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/nlohmann/detail/string_utils.hpp b/include/nlohmann/detail/string_utils.hpp index 4ef748f13..e54a56da6 100644 --- a/include/nlohmann/detail/string_utils.hpp +++ b/include/nlohmann/detail/string_utils.hpp @@ -108,8 +108,8 @@ void encode_utf8(std::uint32_t cp, const Out& out) /////////////////// // UTF-8 decoder states used by decode() below -static constexpr std::uint8_t UTF8_ACCEPT = 0; -static constexpr std::uint8_t UTF8_REJECT = 1; +JSON_INLINE_VARIABLE constexpr std::uint8_t UTF8_ACCEPT = 0; +JSON_INLINE_VARIABLE constexpr std::uint8_t UTF8_REJECT = 1; /*! @brief process a byte of a UTF-8 sequence diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 9de8ff2b2..09f67bdc3 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -6395,8 +6395,8 @@ void encode_utf8(std::uint32_t cp, const Out& out) /////////////////// // UTF-8 decoder states used by decode() below -static constexpr std::uint8_t UTF8_ACCEPT = 0; -static constexpr std::uint8_t UTF8_REJECT = 1; +JSON_INLINE_VARIABLE constexpr std::uint8_t UTF8_ACCEPT = 0; +JSON_INLINE_VARIABLE constexpr std::uint8_t UTF8_REJECT = 1; /*! @brief process a byte of a UTF-8 sequence From 74aefc3486c6dcd03a5a039d86f1ac8d146c0f74 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 00:05:02 +0200 Subject: [PATCH 6/6] Move custom object key tests out of unit-cbor.cpp and unit-msgpack.cpp (#5798) The custom object key tests from #5328 instantiate a second basic_json specialization in unit-cbor.cpp and unit-msgpack.cpp. This pushed unit-msgpack.cpp.obj past 65535 sections in the clang (MinGW) jobs of the Windows workflow, and linking test-msgpack_cpp11 fails with "relocation truncated to fit: IMAGE_REL_AMD64_REL32 against `.rdata'". The GNU linker keeps the section an associative COMDAT section belongs to in 16 bits (x_associated in include/coff/internal.h), although big object files store 32 bits. In larger objects it therefore ties the jump tables of inline functions to the wrong function and discards them together with that function's duplicate. Move the four tests unchanged into unit-custom-object-key-type.cpp and document the limit in windows.yml. Signed-off-by: Niels Lohmann --- .github/workflows/windows.yml | 7 ++ tests/src/unit-cbor.cpp | 53 --------- tests/src/unit-custom-object-key-type.cpp | 127 ++++++++++++++++++++++ tests/src/unit-msgpack.cpp | 53 --------- 4 files changed, 134 insertions(+), 106 deletions(-) create mode 100644 tests/src/unit-custom-object-key-type.cpp diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 855d8af95..c5220e66c 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -166,6 +166,13 @@ jobs: # link, but the binaries clang 11.0.1 and clang 18.1.8 then produce crash # before doctest prints its first line - 39 of 102 tests on clang 18. # Keep the objects small by splitting the test files instead. + # Objects with more than 65535 sections fail to link with "relocation + # truncated to fit: IMAGE_REL_AMD64_REL32 against `.rdata'": the MinGW + # linker stores the section an associative COMDAT section belongs to in + # 16 bits (x_associated in binutils' include/coff/internal.h), so it + # discards the jump tables of inline functions together with the wrong + # function. Each basic_json specialization a test file instantiates adds + # many sections, so test a second one in a file of its own. - name: Run CMake run: cmake -S . -B build ^ -DCMAKE_CXX_COMPILER="C:/Program Files/LLVM/bin/clang++.exe" ^ diff --git a/tests/src/unit-cbor.cpp b/tests/src/unit-cbor.cpp index 9b57d0664..9ee371495 100644 --- a/tests/src/unit-cbor.cpp +++ b/tests/src/unit-cbor.cpp @@ -28,7 +28,6 @@ using nlohmann::json; #include "make_test_data_available.hpp" #include "round_trip_corpus.hpp" #include "test_utils.hpp" -#include "custom_object_key_type.hpp" #include "sax_countdown.hpp" using utils::SaxCountdown; @@ -3358,55 +3357,3 @@ TEST_CASE("CBOR large strings and binaries (chunked reader)") } } } - -TEST_CASE("CBOR supports custom object key types") -{ - using custom_json = custom_object_key_test::json; - using custom_key = custom_object_key_test::key; - - custom_json::object_t object; - object.emplace(custom_key{"short"}, 1); - object.emplace( - custom_key{"a key longer than twenty-three characters"}, - 2); - - const custom_json value(std::move(object)); - const auto encoded = custom_json::to_cbor(value); - - CHECK(nlohmann::json::from_cbor(encoded) == nlohmann::json - { - {"short", 1}, - {"a key longer than twenty-three characters", 2} - }); -} - -TEST_CASE("CBOR supports custom object key types nested deeper than the recursion depth limit") -{ - // below detail::recursion_depth_limit(), keys are written by - // write_cbor_iterative instead of write_cbor - using custom_json = custom_object_key_test::json; - using custom_key = custom_object_key_test::key; - - const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 10; - - custom_json value = 1; - nlohmann::json expected = 1; - for (std::size_t i = 0; i < depth; ++i) - { - // alternate short keys with ones long enough to need a length byte - const std::string name = (i % 2 == 0) ? "k" + std::to_string(i) - : "a key longer than thirty-one characters " + std::to_string(i); - - custom_json::object_t object; - object.emplace(custom_key{name}, std::move(value)); - value = custom_json(std::move(object)); - - nlohmann::json::object_t expected_object; - expected_object.emplace(name, std::move(expected)); - expected = nlohmann::json(std::move(expected_object)); - } - - const auto encoded = custom_json::to_cbor(value); - CHECK(encoded == nlohmann::json::to_cbor(expected)); - CHECK(nlohmann::json::from_cbor(encoded) == expected); -} 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..9c64ebce0 --- /dev/null +++ b/tests/src/unit-custom-object-key-type.cpp @@ -0,0 +1,127 @@ +// __ _____ _____ _____ +// __| | __| | | | 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 "custom_object_key_type.hpp" + +// These tests instantiate a second basic_json specialization. They live in +// their own file rather than in unit-cbor.cpp and unit-msgpack.cpp to keep +// those objects below 65535 sections: the MinGW linker stores the section a +// COMDAT section is associated with in 16 bits, so it misplaces the jump +// tables of larger objects (see the clang job in windows.yml). + +TEST_CASE("CBOR supports custom object key types") +{ + using custom_json = custom_object_key_test::json; + using custom_key = custom_object_key_test::key; + + custom_json::object_t object; + object.emplace(custom_key{"short"}, 1); + object.emplace( + custom_key{"a key longer than twenty-three characters"}, + 2); + + const custom_json value(std::move(object)); + const auto encoded = custom_json::to_cbor(value); + + CHECK(nlohmann::json::from_cbor(encoded) == nlohmann::json + { + {"short", 1}, + {"a key longer than twenty-three characters", 2} + }); +} + +TEST_CASE("CBOR supports custom object key types nested deeper than the recursion depth limit") +{ + // below detail::recursion_depth_limit(), keys are written by + // write_cbor_iterative instead of write_cbor + using custom_json = custom_object_key_test::json; + using custom_key = custom_object_key_test::key; + + const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 10; + + custom_json value = 1; + nlohmann::json expected = 1; + for (std::size_t i = 0; i < depth; ++i) + { + // alternate short keys with ones long enough to need a length byte + const std::string name = (i % 2 == 0) ? "k" + std::to_string(i) + : "a key longer than thirty-one characters " + std::to_string(i); + + custom_json::object_t object; + object.emplace(custom_key{name}, std::move(value)); + value = custom_json(std::move(object)); + + nlohmann::json::object_t expected_object; + expected_object.emplace(name, std::move(expected)); + expected = nlohmann::json(std::move(expected_object)); + } + + const auto encoded = custom_json::to_cbor(value); + CHECK(encoded == nlohmann::json::to_cbor(expected)); + CHECK(nlohmann::json::from_cbor(encoded) == expected); +} + +TEST_CASE("MessagePack supports custom object key types") +{ + using custom_json = custom_object_key_test::json; + using custom_key = custom_object_key_test::key; + + custom_json::object_t object; + object.emplace(custom_key{"short"}, 1); + object.emplace( + custom_key{"a key longer than thirty-one characters"}, + 2); + + const custom_json value(std::move(object)); + const auto encoded = custom_json::to_msgpack(value); + + CHECK(nlohmann::json::from_msgpack(encoded) == nlohmann::json + { + {"short", 1}, + {"a key longer than thirty-one characters", 2} + }); +} + +TEST_CASE("MessagePack supports custom object key types nested deeper than the recursion depth limit") +{ + // below detail::recursion_depth_limit(), keys are written by + // write_msgpack_iterative instead of write_msgpack + using custom_json = custom_object_key_test::json; + using custom_key = custom_object_key_test::key; + + const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 10; + + custom_json value = 1; + nlohmann::json expected = 1; + for (std::size_t i = 0; i < depth; ++i) + { + // alternate short keys with ones long enough to need a length byte + const std::string name = (i % 2 == 0) ? "k" + std::to_string(i) + : "a key longer than thirty-one characters " + std::to_string(i); + + custom_json::object_t object; + object.emplace(custom_key{name}, std::move(value)); + value = custom_json(std::move(object)); + + nlohmann::json::object_t expected_object; + expected_object.emplace(name, std::move(expected)); + expected = nlohmann::json(std::move(expected_object)); + } + + const auto encoded = custom_json::to_msgpack(value); + CHECK(encoded == nlohmann::json::to_msgpack(expected)); + CHECK(nlohmann::json::from_msgpack(encoded) == expected); +} diff --git a/tests/src/unit-msgpack.cpp b/tests/src/unit-msgpack.cpp index 1e1005ad7..aeb0116d3 100644 --- a/tests/src/unit-msgpack.cpp +++ b/tests/src/unit-msgpack.cpp @@ -31,7 +31,6 @@ using nlohmann::json; #include "make_test_data_available.hpp" #include "round_trip_corpus.hpp" #include "test_utils.hpp" -#include "custom_object_key_type.hpp" #include "sax_countdown.hpp" using utils::SaxCountdown; @@ -2483,55 +2482,3 @@ TEST_CASE("MessagePack large strings and binaries (chunked reader)") } } } - -TEST_CASE("MessagePack supports custom object key types") -{ - using custom_json = custom_object_key_test::json; - using custom_key = custom_object_key_test::key; - - custom_json::object_t object; - object.emplace(custom_key{"short"}, 1); - object.emplace( - custom_key{"a key longer than thirty-one characters"}, - 2); - - const custom_json value(std::move(object)); - const auto encoded = custom_json::to_msgpack(value); - - CHECK(nlohmann::json::from_msgpack(encoded) == nlohmann::json - { - {"short", 1}, - {"a key longer than thirty-one characters", 2} - }); -} - -TEST_CASE("MessagePack supports custom object key types nested deeper than the recursion depth limit") -{ - // below detail::recursion_depth_limit(), keys are written by - // write_msgpack_iterative instead of write_msgpack - using custom_json = custom_object_key_test::json; - using custom_key = custom_object_key_test::key; - - const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 10; - - custom_json value = 1; - nlohmann::json expected = 1; - for (std::size_t i = 0; i < depth; ++i) - { - // alternate short keys with ones long enough to need a length byte - const std::string name = (i % 2 == 0) ? "k" + std::to_string(i) - : "a key longer than thirty-one characters " + std::to_string(i); - - custom_json::object_t object; - object.emplace(custom_key{name}, std::move(value)); - value = custom_json(std::move(object)); - - nlohmann::json::object_t expected_object; - expected_object.emplace(name, std::move(expected)); - expected = nlohmann::json(std::move(expected_object)); - } - - const auto encoded = custom_json::to_msgpack(value); - CHECK(encoded == nlohmann::json::to_msgpack(expected)); - CHECK(nlohmann::json::from_msgpack(encoded) == expected); -}