From d8dfc0d0f9cf1097dd2768e3a8770bbddcc0aa08 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Thu, 8 Oct 2026 16:21:59 +0200 Subject: [PATCH 01/34] Fix unused-result warnings in the contains and parse_error examples (#5788) contains(json_pointer) is marked JSON_HEDLEY_WARN_UNUSED_RESULT since #5477, and parse() has been for longer. The contains example ignored the result in two try blocks waiting for a parse_error that contains() never throws, so they printed nothing; print the result for those pointers instead. Signed-off-by: Niels Lohmann --- .../docs/examples/contains__json_pointer.cpp | 26 ++++--------------- .../examples/contains__json_pointer.output | 2 ++ docs/mkdocs/docs/examples/parse_error.cpp | 2 +- 3 files changed, 8 insertions(+), 22 deletions(-) diff --git a/docs/mkdocs/docs/examples/contains__json_pointer.cpp b/docs/mkdocs/docs/examples/contains__json_pointer.cpp index 14d8514b4..985ad9ec5 100644 --- a/docs/mkdocs/docs/examples/contains__json_pointer.cpp +++ b/docs/mkdocs/docs/examples/contains__json_pointer.cpp @@ -19,25 +19,9 @@ int main() << j.contains("/array/1"_json_pointer) << '\n' << j.contains("/array/-"_json_pointer) << '\n' << j.contains("/array/4"_json_pointer) << '\n' - << j.contains("/baz"_json_pointer) << std::endl; - - try - { - // try to use an array index with leading '0' - j.contains("/array/01"_json_pointer); - } - catch (const json::parse_error& e) - { - std::cout << e.what() << '\n'; - } - - try - { - // try to use an array index that is not a number - j.contains("/array/one"_json_pointer); - } - catch (const json::parse_error& e) - { - std::cout << e.what() << '\n'; - } + << j.contains("/baz"_json_pointer) << '\n' + // an array index with a leading '0' is not found + << j.contains("/array/01"_json_pointer) << '\n' + // an array index that is not a number is not found + << j.contains("/array/one"_json_pointer) << std::endl; } diff --git a/docs/mkdocs/docs/examples/contains__json_pointer.output b/docs/mkdocs/docs/examples/contains__json_pointer.output index dd1eb38c1..b989a7c30 100644 --- a/docs/mkdocs/docs/examples/contains__json_pointer.output +++ b/docs/mkdocs/docs/examples/contains__json_pointer.output @@ -5,3 +5,5 @@ true false false false +false +false diff --git a/docs/mkdocs/docs/examples/parse_error.cpp b/docs/mkdocs/docs/examples/parse_error.cpp index ce15ebe22..2f73ac65f 100644 --- a/docs/mkdocs/docs/examples/parse_error.cpp +++ b/docs/mkdocs/docs/examples/parse_error.cpp @@ -8,7 +8,7 @@ int main() try { // parsing input with a syntax error - json::parse("[1,2,3,]"); + json j = json::parse("[1,2,3,]"); } catch (const json::parse_error& e) { From d33068da730aa1ffa3f1acb332e36653681efa2c Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Thu, 8 Oct 2026 17:44:34 +0200 Subject: [PATCH 02/34] Address review comments on the API stability docs and a test comment (#5784) * Address review comments on #5775 and #5779 Allow new defaulted parameters and new default arguments in the API stability rules, mention the macro opt-in, and drop the redundant recompile advice. Describe test-diagnostics-optimized as the regression test for the fixed #5742. Signed-off-by: Niels Lohmann * Document what counts as a breaking change in the API stability rules Spell out the 3.x compatibility rules in the roadmap: new defaulted parameters, new default arguments, noexcept/constexpr, template parameters, parse and dump results, accepted input, key iteration order, iterator invalidation, implicit conversions, to_json/from_json lookup, json_sax, value_t enumerators, and documented macros, CMake options and headers. Also list std::hash values as not part of the public API, and link the macro overview from the section. Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/community/roadmap.md | 32 +++++++++++++++++++-------- tests/CMakeLists.txt | 3 ++- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/docs/mkdocs/docs/community/roadmap.md b/docs/mkdocs/docs/community/roadmap.md index 27afeb4c5..1b1f287a9 100644 --- a/docs/mkdocs/docs/community/roadmap.md +++ b/docs/mkdocs/docs/community/roadmap.md @@ -36,13 +36,26 @@ work items are tracked in the [GitHub milestones](https://github.com/nlohmann/js ## API stability Releases follow [semantic versioning](https://semver.org): a minor or patch release of version 3.x does not break code -that uses the public API. In particular, a 3.x release does not: +that uses the public API, unless that code opts in to a change with a macro as described [below](#version-40). In +particular, a 3.x release does not: -- change the signature of a function (its parameter types, return type, number of parameters, or the const-ness of a - member function); -- remove or rename a function or class; +- make breaking changes to the signature of a function: the types or order of its existing parameters, its return type, + its `noexcept` or `constexpr` specifier, or the const-ness of a member function. New parameters may be added if they + have a default value; +- remove or rename a function or class, or change the template parameters of a public class template; - change which exceptions a function throws, or the [exception ids](../home/exceptions.md); -- change access specifiers or default arguments. +- change access specifiers, or change or remove existing default arguments. New default arguments may be added; +- change the JSON type that a valid input parses to, or the text that `dump()` produces for a valid value; +- accept input that was rejected before, or reject input that was accepted before; +- change the order in which the keys of an object are iterated. The default type sorts keys, and + [`ordered_json`](../api/ordered_json.md) keeps insertion order; +- change when iterators, pointers, or references are invalidated, or the state of a moved-from `basic_json`; +- add or remove implicit conversions from `basic_json`; +- change how `to_json` and `from_json` functions are found, or the behavior of + [`adl_serializer`](../api/adl_serializer/index.md); +- add pure virtual functions to the [`json_sax`](../api/json_sax/index.md) interface; +- remove, rename, renumber, or add enumerators of `value_t`; +- remove or rename a documented macro, CMake option, CMake target, or header, or change what a documented macro does. Exceptions to these rules, for instance when fixing a bug requires changing the exception a function throws, are documented in the [release notes](../home/releases.md). @@ -51,13 +64,14 @@ The following are **not** part of the public API and may change in any release, - The text of exception messages returned by `what()`. Use the [exception id](../home/exceptions.md) to tell errors apart. -- The ABI, including `sizeof(basic_json)` and the memory layout of its values. Recompile your code when you upgrade the - library. The [versioned inline namespace](../features/namespace.md) turns mixing versions into a link error. +- The ABI, including `sizeof(basic_json)` and the memory layout of its values. The + [versioned inline namespace](../features/namespace.md) turns mixing versions into a link error. +- The hash values returned by `std::hash` for `basic_json`. Numbers that compare equal still hash equally. - Everything in namespace `nlohmann::detail`, and macros and type traits that are not documented in the [API reference](../api/basic_json/index.md). -Changes that would break the public API are only added behind a macro whose default keeps the 3.x behavior, see -[Version 4.0](#version-40). +Breaking changes are only added behind a macro whose default keeps the 3.x behavior. See [Version 4.0](#version-40) and +the [macro overview](../features/macros.md). ## Version 4.0 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e092616b7..9318838bb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -138,7 +138,8 @@ json_test_set_test_options(test-disabled_exceptions # only the #972 regression test needs thirdparty/fifo_map on its include path json_test_set_test_options(test-regression1 LINK_LIBRARIES fifo_map_include) -# GCC's false -Warray-bounds error with JSON_DIAGNOSTICS only shows up when optimizing (#5742). +# Regression test for GCC's false -Warray-bounds error with JSON_DIAGNOSTICS (#5742, fixed in #5585). It only +# showed up when optimizing, so build this test with -O3 and the warning as an error. # -O3 makes the optimizer-driven warnings of the ci_test_gcc flag set (-Winline, # -Wsuggest-attribute=...) fire on the library's inline functions; they are not # what this test checks, so turn them off for it. From 69a0c1b82ca0d5c7e3e82524ce09abd407b4a0d6 Mon Sep 17 00:00:00 2001 From: Alex Prabhat Bara Date: Fri, 9 Oct 2026 16:52:07 +0530 Subject: [PATCH 03/34] Avoid allocating temporary basic_json for cbor and msgpack object keys (#5328) * avoid allocating temporary basic_json for CBOR and MessagePack object keys Signed-off-by: alexprabhat99 * add size() to the custom object key test type UBJSON and BJData access object keys through size() and c_str() directly, so the key type now provides both and the comment says why. Signed-off-by: alexprabhat99 * address review: drop key size()/c_str(), test keys below the depth limit Nothing in the library calls size() or c_str() on an object key, so the test key type only keeps data(), which JSON_DIAGNOSTICS needs. The CBOR and MessagePack custom key tests now also nest objects deeper than detail::recursion_depth_limit(), so keys written by write_cbor_iterative and write_msgpack_iterative are covered as well. Signed-off-by: alexprabhat99 --------- Signed-off-by: alexprabhat99 --- docs/mkdocs/docs/api/basic_json/object_t.md | 3 +- .../nlohmann/detail/output/binary_writer.hpp | 180 ++++++++++-------- single_include/nlohmann/json.hpp | 180 ++++++++++-------- tests/src/custom_object_key_type.hpp | 75 ++++++++ tests/src/unit-cbor.cpp | 53 ++++++ tests/src/unit-msgpack.cpp | 53 ++++++ 6 files changed, 393 insertions(+), 151 deletions(-) create mode 100644 tests/src/custom_object_key_type.hpp diff --git a/docs/mkdocs/docs/api/basic_json/object_t.md b/docs/mkdocs/docs/api/basic_json/object_t.md index 204e5ab28..5e7cab9e9 100644 --- a/docs/mkdocs/docs/api/basic_json/object_t.md +++ b/docs/mkdocs/docs/api/basic_json/object_t.md @@ -26,7 +26,8 @@ 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. + order elements inside the container. `object_t::key_type` must be implicitly convertible to `string_t` (required by the + binary formats). `AllocatorType` : the allocator to use for objects (e.g., `std::allocator`) diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index 1cb64f08d..4af3ff9fb 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -244,16 +244,7 @@ class binary_writer case value_t::string: { - string_t storage; - const string_t& value = sanitize_utf8_for_write(*j.m_data.m_value.string, j, storage); - - // step 1: write control byte and the string length - write_cbor_head(0x60, value.size()); - - // step 2: write the string - oa.write_characters( - reinterpret_cast(value.data()), - value.size()); + write_cbor_string(*j.m_data.m_value.string, j); break; } @@ -316,23 +307,20 @@ 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()); for (const auto& el : *j.m_data.m_value.object) { - // el.first is checked here, against the object as - // diagnostics context, because write_cbor(el.first) - // converts it to a temporary basic_json that would be - // used as the context instead; for error_handler_t::keep - // and ::replace/::ignore the recursive write_cbor(el.first) - // call below handles the key like any other string, so no - // separate check is needed here for those - if (error_handler == error_handler_t::strict) - { - check_utf8(el.first, j); - } - write_cbor(el.first); + // 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(el.second, depth + 1); } break; @@ -491,39 +479,7 @@ class binary_writer case value_t::string: { - string_t storage; - const string_t& value = sanitize_utf8_for_write(*j.m_data.m_value.string, j, storage); - - // step 1: write control byte and the string length - const auto N = to_msgpack_length(value.size(), j); - if (N <= 31) - { - // fixstr - write_number(static_cast(0xA0 | N)); - } - else if (N <= (std::numeric_limits::max)()) - { - // str 8 - oa.write_character(to_char_type(0xD9)); - write_number(static_cast(N)); - } - else if (N <= (std::numeric_limits::max)()) - { - // str 16 - oa.write_character(to_char_type(0xDA)); - write_number(static_cast(N)); - } - else - { - // str 32 - oa.write_character(to_char_type(0xDB)); - write_number(static_cast(N)); - } - - // step 2: write the string - oa.write_characters( - reinterpret_cast(value.data()), - value.size()); + write_msgpack_string(*j.m_data.m_value.string, j); break; } @@ -629,19 +585,20 @@ 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); for (const auto& el : *j.m_data.m_value.object) { - // as in write_cbor, el.first is checked here against the - // object as diagnostics context; the recursive call below - // handles keep/replace/ignore like any other string - if (error_handler == error_handler_t::strict) - { - check_utf8(el.first, j); - } - write_msgpack(el.first); + // as in write_cbor, el.first is written directly with the + // object as diagnostics context + write_msgpack_string(el.first, j); write_msgpack(el.second, depth + 1); } break; @@ -1025,13 +982,9 @@ class binary_writer continue; } - // el.first is checked here, against the object as diagnostics - // context, like the matching check in write_cbor's object case - if (error_handler == error_handler_t::strict) - { - check_utf8(current.object_it->first, *current.value); - } - write_cbor(current.object_it->first); + // 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); const BasicJsonType* child = &(current.object_it->second); ++stack.back().object_it; write_cbor_value_or_push(*child, stack); @@ -1106,11 +1059,9 @@ class binary_writer continue; } - if (error_handler == error_handler_t::strict) - { - check_utf8(current.object_it->first, *current.value); - } - write_msgpack(current.object_it->first); + // 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); const BasicJsonType* child = &(current.object_it->second); ++stack.back().object_it; write_msgpack_value_or_push(*child, stack); @@ -1988,6 +1939,85 @@ class binary_writer } } + /*! + @brief write a CBOR text string + + @a value is checked or sanitized according to @ref error_handler, with + @a context (the string value itself, or the object a key belongs to) used + as diagnostics context; this avoids converting object keys to a temporary + basic_json just to write them + + @note When object_t::key_type is not string_t, @a value is a temporary + string_t converted from the key, which lives only until the end of + the caller's statement. The reference returned by + @ref sanitize_utf8_for_write may refer to it, so it must not escape + this function. + */ + void write_cbor_string(const string_t& value, const BasicJsonType& context) + { + string_t storage; + const string_t& sanitized = sanitize_utf8_for_write(value, context, storage); + + // step 1: write control byte and the string length + write_cbor_head(0x60, sanitized.size()); + + // step 2: write the string + oa.write_characters( + reinterpret_cast(sanitized.data()), + sanitized.size()); + } + + ///////////// + // MsgPack // + ///////////// + + /*! + @brief write a MessagePack str + + @a value is checked or sanitized according to @ref error_handler, with + @a context used as diagnostics context, as in @ref write_cbor_string + + @note As in @ref write_cbor_string, @a value may be a temporary string_t + converted from a key, so the reference returned by + @ref sanitize_utf8_for_write must not escape this function. + */ + void write_msgpack_string(const string_t& value, const BasicJsonType& context) + { + string_t storage; + const string_t& sanitized = sanitize_utf8_for_write(value, context, storage); + + // step 1: write control byte and the string length + const auto N = to_msgpack_length(sanitized.size(), context); + if (N <= 31) + { + // fixstr + write_number(static_cast(0xA0 | N)); + } + else if (N <= (std::numeric_limits::max)()) + { + // str 8 + oa.write_character(to_char_type(0xD9)); + write_number(static_cast(N)); + } + else if (N <= (std::numeric_limits::max)()) + { + // str 16 + oa.write_character(to_char_type(0xDA)); + write_number(static_cast(N)); + } + else + { + // str 32 + oa.write_character(to_char_type(0xDB)); + write_number(static_cast(N)); + } + + // step 2: write the string + oa.write_characters( + reinterpret_cast(sanitized.data()), + sanitized.size()); + } + //////////// // UBJSON // //////////// diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 72c95feea..428a6abbf 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -21749,16 +21749,7 @@ class binary_writer case value_t::string: { - string_t storage; - const string_t& value = sanitize_utf8_for_write(*j.m_data.m_value.string, j, storage); - - // step 1: write control byte and the string length - write_cbor_head(0x60, value.size()); - - // step 2: write the string - oa.write_characters( - reinterpret_cast(value.data()), - value.size()); + write_cbor_string(*j.m_data.m_value.string, j); break; } @@ -21821,23 +21812,20 @@ 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()); for (const auto& el : *j.m_data.m_value.object) { - // el.first is checked here, against the object as - // diagnostics context, because write_cbor(el.first) - // converts it to a temporary basic_json that would be - // used as the context instead; for error_handler_t::keep - // and ::replace/::ignore the recursive write_cbor(el.first) - // call below handles the key like any other string, so no - // separate check is needed here for those - if (error_handler == error_handler_t::strict) - { - check_utf8(el.first, j); - } - write_cbor(el.first); + // 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(el.second, depth + 1); } break; @@ -21996,39 +21984,7 @@ class binary_writer case value_t::string: { - string_t storage; - const string_t& value = sanitize_utf8_for_write(*j.m_data.m_value.string, j, storage); - - // step 1: write control byte and the string length - const auto N = to_msgpack_length(value.size(), j); - if (N <= 31) - { - // fixstr - write_number(static_cast(0xA0 | N)); - } - else if (N <= (std::numeric_limits::max)()) - { - // str 8 - oa.write_character(to_char_type(0xD9)); - write_number(static_cast(N)); - } - else if (N <= (std::numeric_limits::max)()) - { - // str 16 - oa.write_character(to_char_type(0xDA)); - write_number(static_cast(N)); - } - else - { - // str 32 - oa.write_character(to_char_type(0xDB)); - write_number(static_cast(N)); - } - - // step 2: write the string - oa.write_characters( - reinterpret_cast(value.data()), - value.size()); + write_msgpack_string(*j.m_data.m_value.string, j); break; } @@ -22134,19 +22090,20 @@ 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); for (const auto& el : *j.m_data.m_value.object) { - // as in write_cbor, el.first is checked here against the - // object as diagnostics context; the recursive call below - // handles keep/replace/ignore like any other string - if (error_handler == error_handler_t::strict) - { - check_utf8(el.first, j); - } - write_msgpack(el.first); + // as in write_cbor, el.first is written directly with the + // object as diagnostics context + write_msgpack_string(el.first, j); write_msgpack(el.second, depth + 1); } break; @@ -22530,13 +22487,9 @@ class binary_writer continue; } - // el.first is checked here, against the object as diagnostics - // context, like the matching check in write_cbor's object case - if (error_handler == error_handler_t::strict) - { - check_utf8(current.object_it->first, *current.value); - } - write_cbor(current.object_it->first); + // 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); const BasicJsonType* child = &(current.object_it->second); ++stack.back().object_it; write_cbor_value_or_push(*child, stack); @@ -22611,11 +22564,9 @@ class binary_writer continue; } - if (error_handler == error_handler_t::strict) - { - check_utf8(current.object_it->first, *current.value); - } - write_msgpack(current.object_it->first); + // 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); const BasicJsonType* child = &(current.object_it->second); ++stack.back().object_it; write_msgpack_value_or_push(*child, stack); @@ -23493,6 +23444,85 @@ class binary_writer } } + /*! + @brief write a CBOR text string + + @a value is checked or sanitized according to @ref error_handler, with + @a context (the string value itself, or the object a key belongs to) used + as diagnostics context; this avoids converting object keys to a temporary + basic_json just to write them + + @note When object_t::key_type is not string_t, @a value is a temporary + string_t converted from the key, which lives only until the end of + the caller's statement. The reference returned by + @ref sanitize_utf8_for_write may refer to it, so it must not escape + this function. + */ + void write_cbor_string(const string_t& value, const BasicJsonType& context) + { + string_t storage; + const string_t& sanitized = sanitize_utf8_for_write(value, context, storage); + + // step 1: write control byte and the string length + write_cbor_head(0x60, sanitized.size()); + + // step 2: write the string + oa.write_characters( + reinterpret_cast(sanitized.data()), + sanitized.size()); + } + + ///////////// + // MsgPack // + ///////////// + + /*! + @brief write a MessagePack str + + @a value is checked or sanitized according to @ref error_handler, with + @a context used as diagnostics context, as in @ref write_cbor_string + + @note As in @ref write_cbor_string, @a value may be a temporary string_t + converted from a key, so the reference returned by + @ref sanitize_utf8_for_write must not escape this function. + */ + void write_msgpack_string(const string_t& value, const BasicJsonType& context) + { + string_t storage; + const string_t& sanitized = sanitize_utf8_for_write(value, context, storage); + + // step 1: write control byte and the string length + const auto N = to_msgpack_length(sanitized.size(), context); + if (N <= 31) + { + // fixstr + write_number(static_cast(0xA0 | N)); + } + else if (N <= (std::numeric_limits::max)()) + { + // str 8 + oa.write_character(to_char_type(0xD9)); + write_number(static_cast(N)); + } + else if (N <= (std::numeric_limits::max)()) + { + // str 16 + oa.write_character(to_char_type(0xDA)); + write_number(static_cast(N)); + } + else + { + // str 32 + oa.write_character(to_char_type(0xDB)); + write_number(static_cast(N)); + } + + // step 2: write the string + oa.write_characters( + reinterpret_cast(sanitized.data()), + sanitized.size()); + } + //////////// // UBJSON // //////////// diff --git a/tests/src/custom_object_key_type.hpp b/tests/src/custom_object_key_type.hpp new file mode 100644 index 000000000..fa23736d4 --- /dev/null +++ b/tests/src/custom_object_key_type.hpp @@ -0,0 +1,75 @@ +// __ _____ _____ _____ +// __| | __| | | | 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 +#include +#include +#include +#include + +namespace custom_object_key_test +{ +class key +{ + public: + key() = default; + + key(const char* value) + : m_value(value) + {} + + key(std::string value) + : m_value(std::move(value)) + {} + + operator std::string() const + { + return m_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& lhs, const key& rhs) + { + return lhs.m_value < rhs.m_value; + } + + private: + std::string m_value; +}; + +template +class object + : public std::map < + key, + Value, + std::less, // NOLINT(modernize-use-transparent-functors) + typename std::allocator_traits::template rebind_alloc < + std::pair>> +{ + private: + using allocator_type = + typename std::allocator_traits::template rebind_alloc < + std::pair>; + + using base_type = + std::map, allocator_type>; // NOLINT(modernize-use-transparent-functors) + + public: + using base_type::base_type; +}; + +using json = nlohmann::basic_json; +} // namespace custom_object_key_test diff --git a/tests/src/unit-cbor.cpp b/tests/src/unit-cbor.cpp index 9ee371495..9b57d0664 100644 --- a/tests/src/unit-cbor.cpp +++ b/tests/src/unit-cbor.cpp @@ -28,6 +28,7 @@ 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; @@ -3357,3 +3358,55 @@ 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-msgpack.cpp b/tests/src/unit-msgpack.cpp index d29769738..0e07c8ea4 100644 --- a/tests/src/unit-msgpack.cpp +++ b/tests/src/unit-msgpack.cpp @@ -31,6 +31,7 @@ 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; @@ -2522,3 +2523,55 @@ 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); +} From d540750f21899cb063b5ca1b6933cc1e49bdc2fc Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 9 Oct 2026 16:56:14 +0200 Subject: [PATCH 04/34] 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 05/34] 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 06/34] 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 07/34] 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 08/34] 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 30d8ddd65aba303cf746ca3b06bf59d01349e392 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 9 Oct 2026 23:48:40 +0200 Subject: [PATCH 09/34] Fix MSVC C4127 in the lookups of editable json documents Signed-off-by: Niels Lohmann --- include/nlohmann/detail/view/document_data.hpp | 12 ++++++++++++ include/nlohmann/detail/view/lookup.hpp | 7 ++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/include/nlohmann/detail/view/document_data.hpp b/include/nlohmann/detail/view/document_data.hpp index 2e1eac3ef..dd2c6be75 100644 --- a/include/nlohmann/detail/view/document_data.hpp +++ b/include/nlohmann/detail/view/document_data.hpp @@ -217,6 +217,13 @@ struct document_data template struct navigation { + /// whether the walk follows edits (the template argument as a runtime + /// condition: `Editable && ...` is a constant condition for MSVC, C4127) + static NLOHMANN_VIEW_ALWAYS_INLINE bool editable() noexcept + { + return false; + } + static NLOHMANN_VIEW_ALWAYS_INLINE const node* first(const document_data& /*d*/, const node* n) noexcept { return n + 1; @@ -236,6 +243,11 @@ struct navigation template<> struct navigation { + static NLOHMANN_VIEW_ALWAYS_INLINE bool editable() noexcept + { + return true; + } + static NLOHMANN_VIEW_ALWAYS_INLINE const node* first(const document_data& d, const node* n) noexcept { return d.first_child_edited(n); diff --git a/include/nlohmann/detail/view/lookup.hpp b/include/nlohmann/detail/view/lookup.hpp index afc43e95a..5bfe36b94 100644 --- a/include/nlohmann/detail/view/lookup.hpp +++ b/include/nlohmann/detail/view/lookup.hpp @@ -92,7 +92,7 @@ template const node* find_member(const document_data& d, const node* object, const char* key, std::size_t n) noexcept { using nav = navigation; - if (NLOHMANN_VIEW_UNLIKELY(object->extra != 0) && (!Editable || (object->flags & node_flags::moved) == 0)) + if (NLOHMANN_VIEW_UNLIKELY(object->extra != 0) && (!nav::editable() || (object->flags & node_flags::moved) == 0)) { return find_indexed(d, object, key, n); // a large object (whose members have not been edited) } @@ -143,8 +143,9 @@ SizeType to_index(IntegerType idx) noexcept template const node* element_at(const document_data& d, const node* array, std::size_t idx) noexcept { - const node* e = navigation::first(d, array); - if (Editable && (array->flags & node_flags::moved) != 0 && d.edits->moved_cap[array->off] != 0) + using nav = navigation; + const node* e = nav::first(d, array); + if (nav::editable() && (array->flags & node_flags::moved) != 0 && d.edits->moved_cap[array->off] != 0) { return e + idx; // a growable block: one link per element } From 4e2e4f31849757a31bc3a41dd78e2669642c9b88 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 9 Oct 2026 23:51:18 +0200 Subject: [PATCH 10/34] Regenerate the amalgamated headers Signed-off-by: Niels Lohmann --- single_include/nlohmann/json_view.hpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 8557a6960..167ea6f57 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -503,6 +503,13 @@ struct document_data template struct navigation { + /// whether the walk follows edits (the template argument as a runtime + /// condition: `Editable && ...` is a constant condition for MSVC, C4127) + static NLOHMANN_VIEW_ALWAYS_INLINE bool editable() noexcept + { + return false; + } + static NLOHMANN_VIEW_ALWAYS_INLINE const node* first(const document_data& /*d*/, const node* n) noexcept { return n + 1; @@ -522,6 +529,11 @@ struct navigation template<> struct navigation { + static NLOHMANN_VIEW_ALWAYS_INLINE bool editable() noexcept + { + return true; + } + static NLOHMANN_VIEW_ALWAYS_INLINE const node* first(const document_data& d, const node* n) noexcept { return d.first_child_edited(n); @@ -3146,7 +3158,7 @@ template const node* find_member(const document_data& d, const node* object, const char* key, std::size_t n) noexcept { using nav = navigation; - if (NLOHMANN_VIEW_UNLIKELY(object->extra != 0) && (!Editable || (object->flags & node_flags::moved) == 0)) + if (NLOHMANN_VIEW_UNLIKELY(object->extra != 0) && (!nav::editable() || (object->flags & node_flags::moved) == 0)) { return find_indexed(d, object, key, n); // a large object (whose members have not been edited) } @@ -3197,8 +3209,9 @@ SizeType to_index(IntegerType idx) noexcept template const node* element_at(const document_data& d, const node* array, std::size_t idx) noexcept { - const node* e = navigation::first(d, array); - if (Editable && (array->flags & node_flags::moved) != 0 && d.edits->moved_cap[array->off] != 0) + using nav = navigation; + const node* e = nav::first(d, array); + if (nav::editable() && (array->flags & node_flags::moved) != 0 && d.edits->moved_cap[array->off] != 0) { return e + idx; // a growable block: one link per element } From 30712fa758c0a3b77914e1f13ca67af9720d5a40 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 9 Oct 2026 23:51:24 +0200 Subject: [PATCH 11/34] Do not force-inline the view's functions on MSVC __forceinline makes MSVC report C4714 for function templates it cannot inline (get), an error under /WX. The forced inlining is only a performance hint, so use plain inline there. Signed-off-by: Niels Lohmann --- include/nlohmann/detail/view/macro_scope.hpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/include/nlohmann/detail/view/macro_scope.hpp b/include/nlohmann/detail/view/macro_scope.hpp index 8bbae1970..e4b685094 100644 --- a/include/nlohmann/detail/view/macro_scope.hpp +++ b/include/nlohmann/detail/view/macro_scope.hpp @@ -28,7 +28,10 @@ #elif defined(_MSC_VER) #define NLOHMANN_VIEW_LIKELY(x) (x) #define NLOHMANN_VIEW_UNLIKELY(x) (x) - #define NLOHMANN_VIEW_ALWAYS_INLINE __forceinline + // plain inline: __forceinline makes MSVC report C4714 (not inlined) for + // function templates it cannot inline, which is an error under /WX; the + // forced inlining is only a performance hint + #define NLOHMANN_VIEW_ALWAYS_INLINE inline #define NLOHMANN_VIEW_NOINLINE __declspec(noinline) #else #define NLOHMANN_VIEW_LIKELY(x) (x) From 2474843c55461650e6d1a299ecfffc3d6b40cbd8 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 9 Oct 2026 23:51:25 +0200 Subject: [PATCH 12/34] Make json_view.hpp pass include-what-you-use Mark the includes that are needed by design (json.hpp, the macro unscope, for the tuple_size specialization), take size_t from , and drop the forward declaration of basic_json_document that the friend declaration makes redundant. Signed-off-by: Niels Lohmann --- include/nlohmann/json_view.hpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index 256466fa0..d1f83af41 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -24,19 +24,18 @@ #ifndef INCLUDE_NLOHMANN_JSON_VIEW_HPP_ #define INCLUDE_NLOHMANN_JSON_VIEW_HPP_ -#include // size_t -#include // memcpy, strlen +#include // memcpy, size_t, strlen #include // distance, input_iterator_tag, iterator_traits #include // map #include // unique_ptr #include // string -#include // tuple_element, tuple_size +#include // tuple_element, tuple_size // IWYU pragma: keep #include // decay, enable_if, integral_constant, is_arithmetic, is_base_of, is_integral, is_same, remove_cv, remove_extent #include // unordered_map #include // forward, move #include // vector -#include +#include // IWYU pragma: export // the view builds on internals of the library: both must be the same version #if NLOHMANN_JSON_VERSION_MAJOR != 3 || NLOHMANN_JSON_VERSION_MINOR != 12 || NLOHMANN_JSON_VERSION_PATCH != 0 @@ -58,9 +57,6 @@ NLOHMANN_JSON_NAMESPACE_BEGIN -template -class basic_json_document; - /*! @brief read-only handle to one value of a basic_json_document @@ -1074,6 +1070,6 @@ class tuple_element> // NOLINT(cert } // namespace std -#include +#include // IWYU pragma: keep #endif // INCLUDE_NLOHMANN_JSON_VIEW_HPP_ From 7fd79a29e04e38db74f36eae295c64c05e6e89b9 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 9 Oct 2026 23:51:34 +0200 Subject: [PATCH 13/34] Regenerate the amalgamated headers Signed-off-by: Niels Lohmann --- single_include/nlohmann/json_view.hpp | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 97185d18d..2b8b7d9d7 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -24,19 +24,18 @@ #ifndef INCLUDE_NLOHMANN_JSON_VIEW_HPP_ #define INCLUDE_NLOHMANN_JSON_VIEW_HPP_ -#include // size_t -#include // memcpy, strlen +#include // memcpy, size_t, strlen #include // distance, input_iterator_tag, iterator_traits #include // map #include // unique_ptr #include // string -#include // tuple_element, tuple_size +#include // tuple_element, tuple_size // IWYU pragma: keep #include // decay, enable_if, integral_constant, is_arithmetic, is_base_of, is_integral, is_same, remove_cv, remove_extent #include // unordered_map #include // forward, move #include // vector -#include +#include // IWYU pragma: export // the view builds on internals of the library: both must be the same version #if NLOHMANN_JSON_VERSION_MAJOR != 3 || NLOHMANN_JSON_VERSION_MINOR != 12 || NLOHMANN_JSON_VERSION_PATCH != 0 @@ -115,7 +114,10 @@ #elif defined(_MSC_VER) #define NLOHMANN_VIEW_LIKELY(x) (x) #define NLOHMANN_VIEW_UNLIKELY(x) (x) - #define NLOHMANN_VIEW_ALWAYS_INLINE __forceinline + // plain inline: __forceinline makes MSVC report C4714 (not inlined) for + // function templates it cannot inline, which is an error under /WX; the + // forced inlining is only a performance hint + #define NLOHMANN_VIEW_ALWAYS_INLINE inline #define NLOHMANN_VIEW_NOINLINE __declspec(noinline) #else #define NLOHMANN_VIEW_LIKELY(x) (x) @@ -2869,9 +2871,6 @@ NLOHMANN_JSON_NAMESPACE_END NLOHMANN_JSON_NAMESPACE_BEGIN -template -class basic_json_document; - /*! @brief read-only handle to one value of a basic_json_document @@ -3907,6 +3906,6 @@ class tuple_element> // NOLINT(cert #undef NLOHMANN_VIEW_THROW #undef NLOHMANN_VIEW_LITTLE_ENDIAN #undef NLOHMANN_VIEW_REPEAT16 - +// IWYU pragma: keep #endif // INCLUDE_NLOHMANN_JSON_VIEW_HPP_ From 74aefc3486c6dcd03a5a039d86f1ac8d146c0f74 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 00:05:02 +0200 Subject: [PATCH 14/34] 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); -} From 0238cf0a3a8bbd2063b225a6a76ba3f96bc8c232 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 00:09:36 +0200 Subject: [PATCH 15/34] Avoid useless casts of header fields in the image tests Signed-off-by: Niels Lohmann --- tests/src/unit-json_view_image.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/src/unit-json_view_image.cpp b/tests/src/unit-json_view_image.cpp index 5d2e735a9..65d88f6ed 100644 --- a/tests/src/unit-json_view_image.cpp +++ b/tests/src/unit-json_view_image.cpp @@ -80,6 +80,14 @@ std::uint64_t header_field(const std::vector& image, std::size_t o return v; } +// a header field as a size; the cast is from a variable, which GCC's +// -Wuseless-cast does not flag where std::uint64_t and std::size_t coincide +std::size_t header_size_field(const std::vector& image, std::size_t offset) +{ + const std::uint64_t v = header_field(image, offset); + return static_cast(v); +} + void set_header_field(std::vector& image, std::size_t offset, std::uint64_t v) { std::memcpy(image.data() + offset, &v, sizeof(v)); @@ -87,8 +95,7 @@ void set_header_field(std::vector& image, std::size_t offset, std: std::size_t node_count(const std::vector& image) { - const std::uint64_t count = header_field(image, 8); - return static_cast(count); + return header_size_field(image, 8); } std::size_t text_at(const std::vector& image) @@ -739,7 +746,7 @@ TEST_CASE("json_view images: errors") // the NULs after the text and the decoded strings bad = image; - bad[text_at(image) + static_cast(header_field(image, 16))] = 'x'; + bad[text_at(image) + header_size_field(image, 16)] = 'x'; CHECK(load_result(bad, image_check::none) == prefix + "sizes out of range"); bad = image; bad.back() = 'x'; @@ -844,8 +851,8 @@ TEST_CASE("json_view images: check") SECTION("bounds") { - const std::size_t text_size = static_cast(header_field(image, 16)); - const std::size_t arena_size = static_cast(header_field(image, 24)); + const std::size_t text_size = header_size_field(image, 16); + const std::size_t arena_size = header_size_field(image, 24); rejected(corrupted(image, 1, [&](node & n) { n.off = static_cast(text_size + 1); @@ -952,7 +959,7 @@ TEST_CASE("json_view images: check") // invalid UTF-8 in a decoded string b = image; const node s2 = node_at(image, 2); - b[t + static_cast(header_field(image, 16)) + 1 + s2.off] = 0xFF; + b[t + header_size_field(image, 16) + 1 + s2.off] = 0xFF; rejected(b, false); } From a9ef628eb4e41f007ec8761125284d6f8976e308 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 00:12:52 +0200 Subject: [PATCH 16/34] Fix the clang-tidy 22 findings in json_view and its tests Signed-off-by: Niels Lohmann --- include/nlohmann/detail/view/lookup.hpp | 2 +- include/nlohmann/json_view.hpp | 8 +++---- tests/src/unit-json_view.cpp | 30 ++++++++++++------------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/include/nlohmann/detail/view/lookup.hpp b/include/nlohmann/detail/view/lookup.hpp index 8ec1c155b..11ad8c97f 100644 --- a/include/nlohmann/detail/view/lookup.hpp +++ b/include/nlohmann/detail/view/lookup.hpp @@ -127,7 +127,7 @@ template SizeType to_index(IntegerType idx) noexcept { const IntegerType zero = 0; - const auto result = static_cast(idx); + const auto result = static_cast(idx); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): negative values are mapped below return (idx < zero || static_cast(result) != idx) ? (std::numeric_limits::max)() : result; } diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index 1ae86996e..9514b9cd2 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -723,7 +723,7 @@ class basic_json_view // the end is unknown: assume a few bytes per node, the output buffer // grows should the value be larger const auto nodes = static_cast(document_data::after(m_node) - m_node); - return (std::min)(m_doc->size - m_node->off, static_cast(1024) + nodes * 16); + return (std::min)(m_doc->size - m_node->off, static_cast(1024) + (nodes * 16)); } /// the value of the first member with this key, or a discarded view @@ -1183,7 +1183,7 @@ using ordered_json_view = basic_json_view; NLOHMANN_JSON_NAMESPACE_END // tuple protocol for the items of basic_json_view::items() (structured bindings) -namespace std // NOLINT(cert-dcl58-cpp) +namespace std // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) { #if defined(__clang__) @@ -1192,11 +1192,11 @@ namespace std // NOLINT(cert-dcl58-cpp) #pragma clang diagnostic ignored "-Wmismatched-tags" #endif template -class tuple_size<::nlohmann::detail::view::view_item> // NOLINT(cert-dcl58-cpp) +class tuple_size<::nlohmann::detail::view::view_item> // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) : public std::integral_constant {}; template -class tuple_element> // NOLINT(cert-dcl58-cpp) +class tuple_element> // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) { public: using type = decltype(std::declval<::nlohmann::detail::view::view_item>().template get()); diff --git a/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp index 132f7c8d0..3b1878d44 100644 --- a/tests/src/unit-json_view.cpp +++ b/tests/src/unit-json_view.cpp @@ -66,7 +66,7 @@ struct oversized_input using value_type = char; std::size_t claimed; - const char* data() const + const char* data() const // NOLINT(readability-convert-member-functions-to-static): container interface { return "[1]"; } @@ -363,10 +363,10 @@ TEST_CASE("json_view") CHECK(json_document::parse(std::vector(text.begin(), text.end())).owns_source()); // a const rvalue cannot be moved from, and is not borrowed (it may be a // temporary): it is copied, as is a const rvalue of any container - const std::string const_text = text; + const std::string const_text = text; // NOLINT(performance-unnecessary-copy-initialization) const json_document from_const_rvalue = json_document::parse(std::move(const_text)); // NOLINT(performance-move-const-arg,hicpp-move-const-arg) CHECK(from_const_rvalue.owns_source()); - CHECK(from_const_rvalue.source().data() != const_text.data()); + CHECK(from_const_rvalue.source().data() != const_text.data()); // NOLINT(bugprone-use-after-move,hicpp-invalid-access-moved): const, not moved from CHECK(from_const_rvalue.root().materialize() == expected); json_document read_const_rvalue; read_const_rvalue.read(std::move(const_text)); // NOLINT(performance-move-const-arg,hicpp-move-const-arg) @@ -744,7 +744,7 @@ TEST_CASE("json_view element access and iteration") }) { const std::string key(n, 'k'); - const json_document dk = json_document::parse("{\"" + key + "\":1,\"" + key + "x\":2,\"" + key + "\":3,\"" + key + "\":4}"); + const json_document dk = json_document::parse("{\"" + key + "\":1,\"" + key + "x\":2,\"" + key + "\":3,\"" + key + "\":4}"); // NOLINT(performance-inefficient-string-concatenation) CAPTURE(n) CHECK(dk.root()[key].materialize() == 1); CHECK(dk.root().at(key).materialize() == 1); @@ -975,8 +975,8 @@ TEST_CASE("json_view element access and iteration") pairs += std::string(key) + "=" + value.materialize().dump() + ";"; } CHECK(pairs == "a=1;b=[true,false];"); - static_assert(std::tuple_size::value == 2, ""); - static_assert(std::is_same::type, json_view>::value, ""); + static_assert(std::tuple_size::value == 2, "tuple_size of an item is 2"); + static_assert(std::is_same::type, json_view>::value, "the second element of an item is a view"); #endif } } @@ -1177,19 +1177,19 @@ TEST_CASE("json_view values") switch (i % 5) // NOLINT(hicpp-multiway-paths-covered) { case 0: - std::snprintf(buf.data(), buf.size(), "%.17g", d); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + static_cast(std::snprintf(buf.data(), buf.size(), "%.17g", d)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) break; case 1: - std::snprintf(buf.data(), buf.size(), "%.15g", d); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + static_cast(std::snprintf(buf.data(), buf.size(), "%.15g", d)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) break; case 2: - std::snprintf(buf.data(), buf.size(), "%.3e", d); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + static_cast(std::snprintf(buf.data(), buf.size(), "%.3e", d)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) break; case 3: - std::snprintf(buf.data(), buf.size(), "%.25g", d); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + static_cast(std::snprintf(buf.data(), buf.size(), "%.25g", d)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) break; default: - std::snprintf(buf.data(), buf.size(), "%.0f", d); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + static_cast(std::snprintf(buf.data(), buf.size(), "%.0f", d)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) break; } tokens.emplace_back(buf.data()); @@ -1232,7 +1232,7 @@ TEST_CASE("json_view values") const json j = json::parse(text); // user types with from_json, and other types, through basic_json - const record r = v.get(); + const auto r = v.get(); CHECK(r.name == "widget"); CHECK(r.count == 3); CHECK((v["pair"].get>() == j["pair"].get>())); @@ -1500,10 +1500,10 @@ TEST_CASE("json_view dump") { chars += "\xC3\xA9"; } - const json_document d = json_document::parse("{\"a\":\"" + chars + R"(","k\n":1})"); + const json_document d = json_document::parse(R"({"a":")" + chars + R"(","k\n":1})"); const json expected = json::parse("\"" + chars + "\""); CHECK(d.root()["a"].dump(-1, ' ', true) == expected.dump(-1, ' ', true)); - CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + 5000 * 6); + CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6)); } SECTION("streams and discarded views") @@ -1595,7 +1595,7 @@ TEST_CASE("json_view comparison") // discarded values compare as basic_json's do const json discarded(json::value_t::discarded); CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested - CHECK((json_view() == discarded) == (discarded == discarded)); + CHECK((json_view() == discarded) == (discarded == discarded)); // NOLINT(readability-container-size-empty) const json_document null_document = json_document::parse("null"); CHECK(!(json_view() == null_document.root())); // NOLINT(readability-container-size-empty) CHECK(!(null_document.root() == discarded)); From b7f0de016f705b98d8743c0d44152806c77e85b2 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 00:13:01 +0200 Subject: [PATCH 17/34] Regenerate the amalgamated headers Signed-off-by: Niels Lohmann --- single_include/nlohmann/json_view.hpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 9422f75da..8e215c3b9 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -2541,7 +2541,7 @@ template SizeType to_index(IntegerType idx) noexcept { const IntegerType zero = 0; - const auto result = static_cast(idx); + const auto result = static_cast(idx); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): negative values are mapped below return (idx < zero || static_cast(result) != idx) ? (std::numeric_limits::max)() : result; } @@ -4374,7 +4374,7 @@ class basic_json_view // the end is unknown: assume a few bytes per node, the output buffer // grows should the value be larger const auto nodes = static_cast(document_data::after(m_node) - m_node); - return (std::min)(m_doc->size - m_node->off, static_cast(1024) + nodes * 16); + return (std::min)(m_doc->size - m_node->off, static_cast(1024) + (nodes * 16)); } /// the value of the first member with this key, or a discarded view @@ -4834,7 +4834,7 @@ using ordered_json_view = basic_json_view; NLOHMANN_JSON_NAMESPACE_END // tuple protocol for the items of basic_json_view::items() (structured bindings) -namespace std // NOLINT(cert-dcl58-cpp) +namespace std // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) { #if defined(__clang__) @@ -4843,11 +4843,11 @@ namespace std // NOLINT(cert-dcl58-cpp) #pragma clang diagnostic ignored "-Wmismatched-tags" #endif template -class tuple_size<::nlohmann::detail::view::view_item> // NOLINT(cert-dcl58-cpp) +class tuple_size<::nlohmann::detail::view::view_item> // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) : public std::integral_constant {}; template -class tuple_element> // NOLINT(cert-dcl58-cpp) +class tuple_element> // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) { public: using type = decltype(std::declval<::nlohmann::detail::view::view_item>().template get()); From 2f94d7b9505d540bbfe6dcfcd5ce2d15a87c838b Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 00:18:16 +0200 Subject: [PATCH 18/34] Give document_data a user-provided constructor Signed-off-by: Niels Lohmann --- include/nlohmann/detail/view/document_data.hpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/include/nlohmann/detail/view/document_data.hpp b/include/nlohmann/detail/view/document_data.hpp index aa3958e41..13916b577 100644 --- a/include/nlohmann/detail/view/document_data.hpp +++ b/include/nlohmann/detail/view/document_data.hpp @@ -65,7 +65,18 @@ struct document_data } }; - document_data() noexcept = default; + /// user-provided so that the class-type members can be initialized in the + /// member initialization list (-Weffc++ asks for it, and old GCC rejects a + /// defaulted constructor whose exception specification differs from the + /// implicit one); they cannot take default member initializers, which old + /// Clang (3.4-3.6) rejects. The other members have default member + /// initializers. noexcept: create() constructs into raw memory and could not + /// release it if this threw (the std::string default constructors do not + /// allocate). + document_data() noexcept + : arena() // NOLINT(readability-redundant-member-init) + , owned() // NOLINT(readability-redundant-member-init) + {} document_data(const document_data&) = delete; document_data(document_data&&) = delete; document_data& operator=(const document_data&) = delete; From a92a9d7a4876b48a6947c3396dd6bf9d4e0fe5be Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 00:18:17 +0200 Subject: [PATCH 19/34] Fix clang-tidy findings in unit-json_view_builder.cpp Signed-off-by: Niels Lohmann --- tests/src/unit-json_view_builder.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/src/unit-json_view_builder.cpp b/tests/src/unit-json_view_builder.cpp index 8b8518c38..9025fa22f 100644 --- a/tests/src/unit-json_view_builder.cpp +++ b/tests/src/unit-json_view_builder.cpp @@ -275,7 +275,7 @@ TEST_CASE("json_view builder") { "1e39", "-1e39", "3.4028235e38", "-3.4028235e38", "3.4028236e38", "-3.4028236e38", "3.4028234663852886e38", "1e38", "340282356779733661637539395458142568448", "340282356779733661637539395458142568447.99", "0.00034028236e42", - "[1.5e38, 3.5e38]", "{\"a\": 1e-50, \"b\": 1e39}" + "[1.5e38, 3.5e38]", R"({"a": 1e-50, "b": 1e39})" }) { CAPTURE(text) @@ -392,7 +392,7 @@ TEST_CASE("json_view builder") }) { CAPTURE(name) - std::ifstream f(std::string(TEST_DATA_DIRECTORY) + name, std::ios::binary); + const std::ifstream f(std::string(TEST_DATA_DIRECTORY) + name, std::ios::binary); std::stringstream ss; ss << f.rdbuf(); const std::string text = ss.str(); From 38e34eb9607821264c3fd9acc758407ff5f392cb Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 00:18:21 +0200 Subject: [PATCH 20/34] Regenerate the amalgamated headers Signed-off-by: Niels Lohmann --- single_include/nlohmann/json_view.hpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 3abcec76d..028e2e8c0 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -315,7 +315,18 @@ struct document_data } }; - document_data() noexcept = default; + /// user-provided so that the class-type members can be initialized in the + /// member initialization list (-Weffc++ asks for it, and old GCC rejects a + /// defaulted constructor whose exception specification differs from the + /// implicit one); they cannot take default member initializers, which old + /// Clang (3.4-3.6) rejects. The other members have default member + /// initializers. noexcept: create() constructs into raw memory and could not + /// release it if this threw (the std::string default constructors do not + /// allocate). + document_data() noexcept + : arena() // NOLINT(readability-redundant-member-init) + , owned() // NOLINT(readability-redundant-member-init) + {} document_data(const document_data&) = delete; document_data(document_data&&) = delete; document_data& operator=(const document_data&) = delete; From 81ead20445c167a64e6be00a51243a1012594992 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 00:19:40 +0200 Subject: [PATCH 21/34] Annotate intentional patterns in unit-json_view.cpp for clang-tidy Signed-off-by: Niels Lohmann --- tests/src/unit-json_view.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp index ad00cab14..7f6b06f79 100644 --- a/tests/src/unit-json_view.cpp +++ b/tests/src/unit-json_view.cpp @@ -56,7 +56,7 @@ struct oversized_input using value_type = char; std::size_t claimed; - const char* data() const + const char* data() const // NOLINT(readability-convert-member-functions-to-static): container interface { return "[1]"; } @@ -353,10 +353,10 @@ TEST_CASE("json_view") CHECK(json_document::parse(std::vector(text.begin(), text.end())).owns_source()); // a const rvalue cannot be moved from, and is not borrowed (it may be a // temporary): it is copied, as is a const rvalue of any container - const std::string const_text = text; + const std::string const_text = text; // NOLINT(performance-unnecessary-copy-initialization) const json_document from_const_rvalue = json_document::parse(std::move(const_text)); // NOLINT(performance-move-const-arg,hicpp-move-const-arg) CHECK(from_const_rvalue.owns_source()); - CHECK(from_const_rvalue.source().data() != const_text.data()); + CHECK(from_const_rvalue.source().data() != const_text.data()); // NOLINT(bugprone-use-after-move,hicpp-invalid-access-moved): const, not moved from CHECK(from_const_rvalue.root().materialize() == expected); json_document read_const_rvalue; read_const_rvalue.read(std::move(const_text)); // NOLINT(performance-move-const-arg,hicpp-move-const-arg) From 0effbaa7a21210b4150cbb5c122060b45dec0b28 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 01:19:05 +0200 Subject: [PATCH 22/34] Fix clang-tidy 22 findings in the json_view headers Silence bugprone-casting-through-void for the SSE loads (a reinterpret_cast would trip -Wcast-align=strict), use auto for a cast initialiser, add parentheses to a mixed expression, and name bugprone-std-namespace-modification in the NOLINTs of the tuple_size/tuple_element specialisations. Signed-off-by: Niels Lohmann --- include/nlohmann/detail/view/lookup.hpp | 2 +- include/nlohmann/detail/view/simd.hpp | 12 ++++++------ include/nlohmann/json_view.hpp | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/include/nlohmann/detail/view/lookup.hpp b/include/nlohmann/detail/view/lookup.hpp index 8729d2daf..6f9d12afa 100644 --- a/include/nlohmann/detail/view/lookup.hpp +++ b/include/nlohmann/detail/view/lookup.hpp @@ -132,7 +132,7 @@ template SizeType to_index(IntegerType idx) noexcept { const IntegerType zero = 0; - const auto result = static_cast(idx); + const auto result = static_cast(idx); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): idx is an index, not a character return (idx < zero || static_cast(result) != idx) ? (std::numeric_limits::max)() : result; } diff --git a/include/nlohmann/detail/view/simd.hpp b/include/nlohmann/detail/view/simd.hpp index 13ad9c59c..1b16acd5a 100644 --- a/include/nlohmann/detail/view/simd.hpp +++ b/include/nlohmann/detail/view/simd.hpp @@ -98,7 +98,7 @@ NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* vector_plain_run(const unsigned return p + (count_trailing_zeros(bits) >> 2u); } #else - const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); + const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), _mm_cmplt_epi8(in, _mm_set1_epi8(0x20))); const auto bits = static_cast(static_cast(_mm_movemask_epi8(special))); @@ -293,15 +293,15 @@ NLOHMANN_VIEW_SSSE3_TARGET NLOHMANN_VIEW_NOINLINE inline const unsigned char* sc #else // the same with SSSE3 (pshufb for the table lookups; nibbles from 16-bit // shifts, as there are no byte shifts) - const __m128i t1h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_high.data()))); - const __m128i t1l = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_low.data()))); - const __m128i t2h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_2_high.data()))); + const __m128i t1h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_high.data()))); // NOLINT(bugprone-casting-through-void) + const __m128i t1l = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_low.data()))); // NOLINT(bugprone-casting-through-void) + const __m128i t2h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_2_high.data()))); // NOLINT(bugprone-casting-through-void) const __m128i nibble = _mm_set1_epi8(0x0F); const __m128i zero = _mm_setzero_si128(); __m128i prev = zero; while (e - block >= 16) { - const __m128i in = _mm_loadu_si128(static_cast(static_cast(block))); + const __m128i in = _mm_loadu_si128(static_cast(static_cast(block))); // NOLINT(bugprone-casting-through-void) const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), _mm_cmpeq_epi8(_mm_subs_epu8(in, _mm_set1_epi8(0x1F)), zero)); // in < 0x20 const __m128i prev1 = _mm_alignr_epi8(in, prev, 15); @@ -315,7 +315,7 @@ NLOHMANN_VIEW_SSSE3_TARGET NLOHMANN_VIEW_NOINLINE inline const unsigned char* sc const auto err_bits = ~static_cast(_mm_movemask_epi8(_mm_cmpeq_epi8(err, zero))) & 0xFFFFu; if (special_bits != 0) { - const unsigned k = static_cast(count_trailing_zeros(static_cast(special_bits))); + const auto k = static_cast(count_trailing_zeros(static_cast(special_bits))); if ((err_bits & ((2u << k) - 1u)) == 0) { return block + k; diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index 7d66b9ded..937721e1a 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -725,7 +725,7 @@ class basic_json_view // the end is unknown: assume a few bytes per node, the output buffer // grows should the value be larger const auto nodes = static_cast(document_data::after(m_node) - m_node); - return (std::min)(m_doc->size - m_node->off, static_cast(1024) + nodes * 16); + return (std::min)(m_doc->size - m_node->off, static_cast(1024) + (nodes * 16)); } /// the value of the first member with this key, or a discarded view @@ -1212,11 +1212,11 @@ namespace std // NOLINT(cert-dcl58-cpp) #pragma clang diagnostic ignored "-Wmismatched-tags" #endif template -class tuple_size<::nlohmann::detail::view::view_item> // NOLINT(cert-dcl58-cpp) +class tuple_size<::nlohmann::detail::view::view_item> // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) : public std::integral_constant {}; template -class tuple_element> // NOLINT(cert-dcl58-cpp) +class tuple_element> // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) { public: using type = decltype(std::declval<::nlohmann::detail::view::view_item>().template get()); From d81b534bb2c09b52e5b18b236a87d7a7830cceca Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 01:21:22 +0200 Subject: [PATCH 23/34] Fix clang-tidy 22 findings in unit-json_view.cpp and unit-json_view_builder.cpp Signed-off-by: Niels Lohmann --- tests/src/unit-json_view.cpp | 25 +++++++++++++------------ tests/src/unit-json_view_builder.cpp | 4 ++-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp index 19d432650..b0c4212e7 100644 --- a/tests/src/unit-json_view.cpp +++ b/tests/src/unit-json_view.cpp @@ -66,7 +66,7 @@ struct oversized_input using value_type = char; std::size_t claimed; - const char* data() const + const char* data() const // NOLINT(readability-convert-member-functions-to-static): mimics a container { return "[1]"; } @@ -363,13 +363,13 @@ TEST_CASE("json_view") CHECK(json_document::parse(std::vector(text.begin(), text.end())).owns_source()); // a const rvalue cannot be moved from, and is not borrowed (it may be a // temporary): it is copied, as is a const rvalue of any container - const std::string const_text = text; + const std::string const_text = text; // NOLINT(performance-unnecessary-copy-initialization): the copy is the const rvalue under test const json_document from_const_rvalue = json_document::parse(std::move(const_text)); // NOLINT(performance-move-const-arg,hicpp-move-const-arg) CHECK(from_const_rvalue.owns_source()); CHECK(from_const_rvalue.source().data() != const_text.data()); CHECK(from_const_rvalue.root().materialize() == expected); json_document read_const_rvalue; - read_const_rvalue.read(std::move(const_text)); // NOLINT(performance-move-const-arg,hicpp-move-const-arg) + read_const_rvalue.read(std::move(const_text)); // NOLINT(performance-move-const-arg,hicpp-move-const-arg,bugprone-use-after-move,hicpp-invalid-access-moved) CHECK(read_const_rvalue.owns_source()); CHECK(read_const_rvalue.root().materialize() == expected); const std::vector const_chars(text.begin(), text.end()); @@ -744,7 +744,7 @@ TEST_CASE("json_view element access and iteration") }) { const std::string key(n, 'k'); - const json_document dk = json_document::parse("{\"" + key + "\":1,\"" + key + "x\":2,\"" + key + "\":3,\"" + key + "\":4}"); + const json_document dk = json_document::parse("{\"" + key + "\":1,\"" + key + "x\":2,\"" + key + "\":3,\"" + key + "\":4}"); // NOLINT(performance-inefficient-string-concatenation) CAPTURE(n) CHECK(dk.root()[key].materialize() == 1); CHECK(dk.root().at(key).materialize() == 1); @@ -1177,19 +1177,19 @@ TEST_CASE("json_view values") switch (i % 5) // NOLINT(hicpp-multiway-paths-covered) { case 0: - std::snprintf(buf.data(), buf.size(), "%.17g", d); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + static_cast(std::snprintf(buf.data(), buf.size(), "%.17g", d)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) break; case 1: - std::snprintf(buf.data(), buf.size(), "%.15g", d); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + static_cast(std::snprintf(buf.data(), buf.size(), "%.15g", d)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) break; case 2: - std::snprintf(buf.data(), buf.size(), "%.3e", d); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + static_cast(std::snprintf(buf.data(), buf.size(), "%.3e", d)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) break; case 3: - std::snprintf(buf.data(), buf.size(), "%.25g", d); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + static_cast(std::snprintf(buf.data(), buf.size(), "%.25g", d)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) break; default: - std::snprintf(buf.data(), buf.size(), "%.0f", d); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + static_cast(std::snprintf(buf.data(), buf.size(), "%.0f", d)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) break; } tokens.emplace_back(buf.data()); @@ -1500,10 +1500,10 @@ TEST_CASE("json_view dump") { chars += "\xC3\xA9"; } - const json_document d = json_document::parse("{\"a\":\"" + chars + R"(","k\n":1})"); + const json_document d = json_document::parse(R"({"a":")" + chars + R"(","k\n":1})"); const json expected = json::parse("\"" + chars + "\""); CHECK(d.root()["a"].dump(-1, ' ', true) == expected.dump(-1, ' ', true)); - CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + 5000 * 6); + CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6)); } SECTION("streams and discarded views") @@ -1595,7 +1595,7 @@ TEST_CASE("json_view comparison") // discarded values compare as basic_json's do const json discarded(json::value_t::discarded); CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested - CHECK((json_view() == discarded) == (discarded == discarded)); + CHECK((json_view() == discarded) == (discarded == discarded)); // NOLINT(readability-container-size-empty) const json_document null_document = json_document::parse("null"); CHECK(!(json_view() == null_document.root())); // NOLINT(readability-container-size-empty) CHECK(!(null_document.root() == discarded)); @@ -1684,6 +1684,7 @@ TEST_CASE("json_view large objects") ordered.push_back(keys[i]); } std::vector result; + result.reserve(ordered.size()); for (std::size_t i = 0; i < ordered.size(); ++i) { result.push_back({ordered[i], i}); diff --git a/tests/src/unit-json_view_builder.cpp b/tests/src/unit-json_view_builder.cpp index 8b50a1599..320d4dde9 100644 --- a/tests/src/unit-json_view_builder.cpp +++ b/tests/src/unit-json_view_builder.cpp @@ -302,7 +302,7 @@ TEST_CASE("json_view builder") { "1e39", "-1e39", "3.4028235e38", "-3.4028235e38", "3.4028236e38", "-3.4028236e38", "3.4028234663852886e38", "1e38", "340282356779733661637539395458142568448", "340282356779733661637539395458142568447.99", "0.00034028236e42", - "[1.5e38, 3.5e38]", "{\"a\": 1e-50, \"b\": 1e39}" + "[1.5e38, 3.5e38]", R"({"a": 1e-50, "b": 1e39})" }) { CAPTURE(text) @@ -419,7 +419,7 @@ TEST_CASE("json_view builder") }) { CAPTURE(name) - std::ifstream f(std::string(TEST_DATA_DIRECTORY) + name, std::ios::binary); + const std::ifstream f(std::string(TEST_DATA_DIRECTORY) + name, std::ios::binary); std::stringstream ss; ss << f.rdbuf(); const std::string text = ss.str(); From 486ab856d9a8cfc3374863f4e7b70d889bb8b754 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 01:22:41 +0200 Subject: [PATCH 24/34] Regenerate the amalgamated headers Signed-off-by: Niels Lohmann --- single_include/nlohmann/json_view.hpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 2e74e4441..9ad479529 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -545,7 +545,7 @@ NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* vector_plain_run(const unsigned return p + (count_trailing_zeros(bits) >> 2u); } #else - const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); + const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), _mm_cmplt_epi8(in, _mm_set1_epi8(0x20))); const auto bits = static_cast(static_cast(_mm_movemask_epi8(special))); @@ -740,15 +740,15 @@ NLOHMANN_VIEW_SSSE3_TARGET NLOHMANN_VIEW_NOINLINE inline const unsigned char* sc #else // the same with SSSE3 (pshufb for the table lookups; nibbles from 16-bit // shifts, as there are no byte shifts) - const __m128i t1h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_high.data()))); - const __m128i t1l = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_low.data()))); - const __m128i t2h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_2_high.data()))); + const __m128i t1h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_high.data()))); // NOLINT(bugprone-casting-through-void) + const __m128i t1l = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_low.data()))); // NOLINT(bugprone-casting-through-void) + const __m128i t2h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_2_high.data()))); // NOLINT(bugprone-casting-through-void) const __m128i nibble = _mm_set1_epi8(0x0F); const __m128i zero = _mm_setzero_si128(); __m128i prev = zero; while (e - block >= 16) { - const __m128i in = _mm_loadu_si128(static_cast(static_cast(block))); + const __m128i in = _mm_loadu_si128(static_cast(static_cast(block))); // NOLINT(bugprone-casting-through-void) const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), _mm_cmpeq_epi8(_mm_subs_epu8(in, _mm_set1_epi8(0x1F)), zero)); // in < 0x20 const __m128i prev1 = _mm_alignr_epi8(in, prev, 15); @@ -762,7 +762,7 @@ NLOHMANN_VIEW_SSSE3_TARGET NLOHMANN_VIEW_NOINLINE inline const unsigned char* sc const auto err_bits = ~static_cast(_mm_movemask_epi8(_mm_cmpeq_epi8(err, zero))) & 0xFFFFu; if (special_bits != 0) { - const unsigned k = static_cast(count_trailing_zeros(static_cast(special_bits))); + const auto k = static_cast(count_trailing_zeros(static_cast(special_bits))); if ((err_bits & ((2u << k) - 1u)) == 0) { return block + k; @@ -3128,7 +3128,7 @@ template SizeType to_index(IntegerType idx) noexcept { const IntegerType zero = 0; - const auto result = static_cast(idx); + const auto result = static_cast(idx); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): idx is an index, not a character return (idx < zero || static_cast(result) != idx) ? (std::numeric_limits::max)() : result; } @@ -4963,7 +4963,7 @@ class basic_json_view // the end is unknown: assume a few bytes per node, the output buffer // grows should the value be larger const auto nodes = static_cast(document_data::after(m_node) - m_node); - return (std::min)(m_doc->size - m_node->off, static_cast(1024) + nodes * 16); + return (std::min)(m_doc->size - m_node->off, static_cast(1024) + (nodes * 16)); } /// the value of the first member with this key, or a discarded view @@ -5450,11 +5450,11 @@ namespace std // NOLINT(cert-dcl58-cpp) #pragma clang diagnostic ignored "-Wmismatched-tags" #endif template -class tuple_size<::nlohmann::detail::view::view_item> // NOLINT(cert-dcl58-cpp) +class tuple_size<::nlohmann::detail::view::view_item> // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) : public std::integral_constant {}; template -class tuple_element> // NOLINT(cert-dcl58-cpp) +class tuple_element> // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) { public: using type = decltype(std::declval<::nlohmann::detail::view::view_item>().template get()); From efcbab2cf3faa557570c49622a5804633cc8bbd7 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 01:23:25 +0200 Subject: [PATCH 25/34] Fix clang-tidy 22 findings in unit-class_lexer.cpp Signed-off-by: Niels Lohmann --- tests/src/unit-class_lexer.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/src/unit-class_lexer.cpp b/tests/src/unit-class_lexer.cpp index e5f82b53c..daa855d33 100644 --- a/tests/src/unit-class_lexer.cpp +++ b/tests/src/unit-class_lexer.cpp @@ -1794,9 +1794,9 @@ TEST_CASE("string scanning kernels") } // eight bytes as a little-endian word, at any alignment - const unsigned char bytes[16] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF}; - CHECK(nlohmann::detail::read_eight_bytes(bytes) == 0x0807060504030201u); - CHECK(nlohmann::detail::read_eight_bytes(bytes + 1) == 0x0908070605040302u); - CHECK(nlohmann::detail::read_eight_bytes(bytes + 8) == 0xFF0F0E0D0C0B0A09u); - CHECK(nlohmann::detail::read_eight_bytes(reinterpret_cast(bytes) + 3) == 0x0B0A090807060504u); + const std::array bytes = {{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF}}; + CHECK(nlohmann::detail::read_eight_bytes(bytes.data()) == 0x0807060504030201u); + CHECK(nlohmann::detail::read_eight_bytes(bytes.data() + 1) == 0x0908070605040302u); + CHECK(nlohmann::detail::read_eight_bytes(bytes.data() + 8) == 0xFF0F0E0D0C0B0A09u); + CHECK(nlohmann::detail::read_eight_bytes(reinterpret_cast(bytes.data()) + 3) == 0x0B0A090807060504u); } From 558527941a933da75bcb74666f4e9b08963b3c61 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 01:19:05 +0200 Subject: [PATCH 26/34] Fix clang-tidy 22 findings in the json_view headers Silence bugprone-casting-through-void for the SSE loads (a reinterpret_cast would trip -Wcast-align=strict), use auto for a cast initialiser, add parentheses to a mixed expression, and name bugprone-std-namespace-modification in the NOLINTs of the tuple_size/tuple_element specialisations. Signed-off-by: Niels Lohmann (cherry picked from commit 0effbaa7a21210b4150cbb5c122060b45dec0b28) --- include/nlohmann/detail/view/lookup.hpp | 2 +- include/nlohmann/detail/view/simd.hpp | 12 ++++++------ include/nlohmann/json_view.hpp | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/include/nlohmann/detail/view/lookup.hpp b/include/nlohmann/detail/view/lookup.hpp index 5bfe36b94..f7c303654 100644 --- a/include/nlohmann/detail/view/lookup.hpp +++ b/include/nlohmann/detail/view/lookup.hpp @@ -134,7 +134,7 @@ template SizeType to_index(IntegerType idx) noexcept { const IntegerType zero = 0; - const auto result = static_cast(idx); + const auto result = static_cast(idx); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): idx is an index, not a character return (idx < zero || static_cast(result) != idx) ? (std::numeric_limits::max)() : result; } diff --git a/include/nlohmann/detail/view/simd.hpp b/include/nlohmann/detail/view/simd.hpp index 13ad9c59c..1b16acd5a 100644 --- a/include/nlohmann/detail/view/simd.hpp +++ b/include/nlohmann/detail/view/simd.hpp @@ -98,7 +98,7 @@ NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* vector_plain_run(const unsigned return p + (count_trailing_zeros(bits) >> 2u); } #else - const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); + const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), _mm_cmplt_epi8(in, _mm_set1_epi8(0x20))); const auto bits = static_cast(static_cast(_mm_movemask_epi8(special))); @@ -293,15 +293,15 @@ NLOHMANN_VIEW_SSSE3_TARGET NLOHMANN_VIEW_NOINLINE inline const unsigned char* sc #else // the same with SSSE3 (pshufb for the table lookups; nibbles from 16-bit // shifts, as there are no byte shifts) - const __m128i t1h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_high.data()))); - const __m128i t1l = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_low.data()))); - const __m128i t2h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_2_high.data()))); + const __m128i t1h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_high.data()))); // NOLINT(bugprone-casting-through-void) + const __m128i t1l = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_low.data()))); // NOLINT(bugprone-casting-through-void) + const __m128i t2h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_2_high.data()))); // NOLINT(bugprone-casting-through-void) const __m128i nibble = _mm_set1_epi8(0x0F); const __m128i zero = _mm_setzero_si128(); __m128i prev = zero; while (e - block >= 16) { - const __m128i in = _mm_loadu_si128(static_cast(static_cast(block))); + const __m128i in = _mm_loadu_si128(static_cast(static_cast(block))); // NOLINT(bugprone-casting-through-void) const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), _mm_cmpeq_epi8(_mm_subs_epu8(in, _mm_set1_epi8(0x1F)), zero)); // in < 0x20 const __m128i prev1 = _mm_alignr_epi8(in, prev, 15); @@ -315,7 +315,7 @@ NLOHMANN_VIEW_SSSE3_TARGET NLOHMANN_VIEW_NOINLINE inline const unsigned char* sc const auto err_bits = ~static_cast(_mm_movemask_epi8(_mm_cmpeq_epi8(err, zero))) & 0xFFFFu; if (special_bits != 0) { - const unsigned k = static_cast(count_trailing_zeros(static_cast(special_bits))); + const auto k = static_cast(count_trailing_zeros(static_cast(special_bits))); if ((err_bits & ((2u << k) - 1u)) == 0) { return block + k; diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index 5b1a17ddb..302c09d5b 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -759,7 +759,7 @@ class basic_json_view // the end is unknown: assume a few bytes per node, the output buffer // grows should the value be larger const auto nodes = static_cast(document_data::after(m_node) - m_node); - return (std::min)(m_doc->size - m_node->off, static_cast(1024) + nodes * 16); + return (std::min)(m_doc->size - m_node->off, static_cast(1024) + (nodes * 16)); } /// the value of the first member with this key, or a discarded view @@ -1439,11 +1439,11 @@ namespace std // NOLINT(cert-dcl58-cpp) #pragma clang diagnostic ignored "-Wmismatched-tags" #endif template -class tuple_size<::nlohmann::detail::view::view_item> // NOLINT(cert-dcl58-cpp) +class tuple_size<::nlohmann::detail::view::view_item> // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) : public std::integral_constant {}; template -class tuple_element> // NOLINT(cert-dcl58-cpp) +class tuple_element> // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) { public: using type = decltype(std::declval<::nlohmann::detail::view::view_item>().template get()); From cd435ea5d3fbd4f67ef32f1a635937d766973054 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 01:24:40 +0200 Subject: [PATCH 27/34] Fix clang-tidy 22 findings in unit-json_view_edit.cpp and edit.hpp Signed-off-by: Niels Lohmann --- include/nlohmann/detail/view/edit.hpp | 4 ++-- tests/src/unit-json_view_edit.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/include/nlohmann/detail/view/edit.hpp b/include/nlohmann/detail/view/edit.hpp index 7a9daa6bc..fb8d769f3 100644 --- a/include/nlohmann/detail/view/edit.hpp +++ b/include/nlohmann/detail/view/edit.hpp @@ -323,7 +323,7 @@ class editor node* w = h + 1; std::size_t erased = 0; bool kept = false; - for (node* r = h + 1, *end = h + h->next; r != end; r += 2) + for (const node* r = h + 1, *end = h + h->next; r != end; r += 2) { const bool match = key_equals(*r, key); if (match && (kept || !keep_first)) @@ -547,7 +547,7 @@ class editor const bool negative = k == value_t::number_integer && static_cast(bits) < 0; std::uint64_t magnitude = negative ? 0 - bits : bits; std::array buf{}; - char* p = buf.data() + buf.size(); + char* p = buf.data() + buf.size(); // NOLINT(misc-const-correctness): digits are written through p do { *--p = static_cast('0' + (magnitude % 10)); diff --git a/tests/src/unit-json_view_edit.cpp b/tests/src/unit-json_view_edit.cpp index 29be6bb1b..215afd6aa 100644 --- a/tests/src/unit-json_view_edit.cpp +++ b/tests/src/unit-json_view_edit.cpp @@ -265,7 +265,7 @@ void check_lookups(const ordered_json_editable_view& v) } else if (v.is_array()) { - for (std::size_t i = 0; i < v.size(); ++i) + for (std::size_t i = 0; i < v.size(); ++i) // NOLINT(modernize-loop-convert): both v[i] and v.at(i) are tested { check_lookups(v[i]); check_lookups(v.at(i)); From dee4be88d602b550c72f02d4ce9f5881053c6ab7 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 01:24:42 +0200 Subject: [PATCH 28/34] Regenerate the amalgamated headers Signed-off-by: Niels Lohmann --- single_include/nlohmann/json_view.hpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 167ea6f57..995834631 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -680,7 +680,7 @@ NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* vector_plain_run(const unsigned return p + (count_trailing_zeros(bits) >> 2u); } #else - const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); + const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), _mm_cmplt_epi8(in, _mm_set1_epi8(0x20))); const auto bits = static_cast(static_cast(_mm_movemask_epi8(special))); @@ -875,15 +875,15 @@ NLOHMANN_VIEW_SSSE3_TARGET NLOHMANN_VIEW_NOINLINE inline const unsigned char* sc #else // the same with SSSE3 (pshufb for the table lookups; nibbles from 16-bit // shifts, as there are no byte shifts) - const __m128i t1h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_high.data()))); - const __m128i t1l = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_low.data()))); - const __m128i t2h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_2_high.data()))); + const __m128i t1h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_high.data()))); // NOLINT(bugprone-casting-through-void) + const __m128i t1l = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_low.data()))); // NOLINT(bugprone-casting-through-void) + const __m128i t2h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_2_high.data()))); // NOLINT(bugprone-casting-through-void) const __m128i nibble = _mm_set1_epi8(0x0F); const __m128i zero = _mm_setzero_si128(); __m128i prev = zero; while (e - block >= 16) { - const __m128i in = _mm_loadu_si128(static_cast(static_cast(block))); + const __m128i in = _mm_loadu_si128(static_cast(static_cast(block))); // NOLINT(bugprone-casting-through-void) const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), _mm_cmpeq_epi8(_mm_subs_epu8(in, _mm_set1_epi8(0x1F)), zero)); // in < 0x20 const __m128i prev1 = _mm_alignr_epi8(in, prev, 15); @@ -897,7 +897,7 @@ NLOHMANN_VIEW_SSSE3_TARGET NLOHMANN_VIEW_NOINLINE inline const unsigned char* sc const auto err_bits = ~static_cast(_mm_movemask_epi8(_mm_cmpeq_epi8(err, zero))) & 0xFFFFu; if (special_bits != 0) { - const unsigned k = static_cast(count_trailing_zeros(static_cast(special_bits))); + const auto k = static_cast(count_trailing_zeros(static_cast(special_bits))); if ((err_bits & ((2u << k) - 1u)) == 0) { return block + k; @@ -3200,7 +3200,7 @@ template SizeType to_index(IntegerType idx) noexcept { const IntegerType zero = 0; - const auto result = static_cast(idx); + const auto result = static_cast(idx); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): idx is an index, not a character return (idx < zero || static_cast(result) != idx) ? (std::numeric_limits::max)() : result; } @@ -3542,7 +3542,7 @@ class editor node* w = h + 1; std::size_t erased = 0; bool kept = false; - for (node* r = h + 1, *end = h + h->next; r != end; r += 2) + for (const node* r = h + 1, *end = h + h->next; r != end; r += 2) { const bool match = key_equals(*r, key); if (match && (kept || !keep_first)) @@ -3766,7 +3766,7 @@ class editor const bool negative = k == value_t::number_integer && static_cast(bits) < 0; std::uint64_t magnitude = negative ? 0 - bits : bits; std::array buf{}; - char* p = buf.data() + buf.size(); + char* p = buf.data() + buf.size(); // NOLINT(misc-const-correctness): digits are written through p do { *--p = static_cast('0' + (magnitude % 10)); @@ -6337,7 +6337,7 @@ class basic_json_view // the end is unknown: assume a few bytes per node, the output buffer // grows should the value be larger const auto nodes = static_cast(document_data::after(m_node) - m_node); - return (std::min)(m_doc->size - m_node->off, static_cast(1024) + nodes * 16); + return (std::min)(m_doc->size - m_node->off, static_cast(1024) + (nodes * 16)); } /// the value of the first member with this key, or a discarded view @@ -7017,11 +7017,11 @@ namespace std // NOLINT(cert-dcl58-cpp) #pragma clang diagnostic ignored "-Wmismatched-tags" #endif template -class tuple_size<::nlohmann::detail::view::view_item> // NOLINT(cert-dcl58-cpp) +class tuple_size<::nlohmann::detail::view::view_item> // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) : public std::integral_constant {}; template -class tuple_element> // NOLINT(cert-dcl58-cpp) +class tuple_element> // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) { public: using type = decltype(std::declval<::nlohmann::detail::view::view_item>().template get()); From 16963863e2d58d35abf2712157cdc98eaf2bbc3d Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 01:19:05 +0200 Subject: [PATCH 29/34] Fix clang-tidy 22 findings in the json_view headers Silence bugprone-casting-through-void for the SSE loads (a reinterpret_cast would trip -Wcast-align=strict), use auto for a cast initialiser, add parentheses to a mixed expression, and name bugprone-std-namespace-modification in the NOLINTs of the tuple_size/tuple_element specialisations. Signed-off-by: Niels Lohmann (cherry picked from commit 0effbaa7a21210b4150cbb5c122060b45dec0b28) --- include/nlohmann/detail/view/lookup.hpp | 2 +- include/nlohmann/detail/view/simd.hpp | 12 ++++++------ include/nlohmann/json_view.hpp | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/include/nlohmann/detail/view/lookup.hpp b/include/nlohmann/detail/view/lookup.hpp index afc43e95a..46e1a5eda 100644 --- a/include/nlohmann/detail/view/lookup.hpp +++ b/include/nlohmann/detail/view/lookup.hpp @@ -134,7 +134,7 @@ template SizeType to_index(IntegerType idx) noexcept { const IntegerType zero = 0; - const auto result = static_cast(idx); + const auto result = static_cast(idx); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): idx is an index, not a character return (idx < zero || static_cast(result) != idx) ? (std::numeric_limits::max)() : result; } diff --git a/include/nlohmann/detail/view/simd.hpp b/include/nlohmann/detail/view/simd.hpp index 13ad9c59c..1b16acd5a 100644 --- a/include/nlohmann/detail/view/simd.hpp +++ b/include/nlohmann/detail/view/simd.hpp @@ -98,7 +98,7 @@ NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* vector_plain_run(const unsigned return p + (count_trailing_zeros(bits) >> 2u); } #else - const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); + const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), _mm_cmplt_epi8(in, _mm_set1_epi8(0x20))); const auto bits = static_cast(static_cast(_mm_movemask_epi8(special))); @@ -293,15 +293,15 @@ NLOHMANN_VIEW_SSSE3_TARGET NLOHMANN_VIEW_NOINLINE inline const unsigned char* sc #else // the same with SSSE3 (pshufb for the table lookups; nibbles from 16-bit // shifts, as there are no byte shifts) - const __m128i t1h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_high.data()))); - const __m128i t1l = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_low.data()))); - const __m128i t2h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_2_high.data()))); + const __m128i t1h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_high.data()))); // NOLINT(bugprone-casting-through-void) + const __m128i t1l = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_low.data()))); // NOLINT(bugprone-casting-through-void) + const __m128i t2h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_2_high.data()))); // NOLINT(bugprone-casting-through-void) const __m128i nibble = _mm_set1_epi8(0x0F); const __m128i zero = _mm_setzero_si128(); __m128i prev = zero; while (e - block >= 16) { - const __m128i in = _mm_loadu_si128(static_cast(static_cast(block))); + const __m128i in = _mm_loadu_si128(static_cast(static_cast(block))); // NOLINT(bugprone-casting-through-void) const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), _mm_cmpeq_epi8(_mm_subs_epu8(in, _mm_set1_epi8(0x1F)), zero)); // in < 0x20 const __m128i prev1 = _mm_alignr_epi8(in, prev, 15); @@ -315,7 +315,7 @@ NLOHMANN_VIEW_SSSE3_TARGET NLOHMANN_VIEW_NOINLINE inline const unsigned char* sc const auto err_bits = ~static_cast(_mm_movemask_epi8(_mm_cmpeq_epi8(err, zero))) & 0xFFFFu; if (special_bits != 0) { - const unsigned k = static_cast(count_trailing_zeros(static_cast(special_bits))); + const auto k = static_cast(count_trailing_zeros(static_cast(special_bits))); if ((err_bits & ((2u << k) - 1u)) == 0) { return block + k; diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index e1d0f3792..6527f8976 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -760,7 +760,7 @@ class basic_json_view // the end is unknown: assume a few bytes per node, the output buffer // grows should the value be larger const auto nodes = static_cast(document_data::after(m_node) - m_node); - return (std::min)(m_doc->size - m_node->off, static_cast(1024) + nodes * 16); + return (std::min)(m_doc->size - m_node->off, static_cast(1024) + (nodes * 16)); } /// the value of the first member with this key, or a discarded view @@ -1496,11 +1496,11 @@ namespace std // NOLINT(cert-dcl58-cpp) #pragma clang diagnostic ignored "-Wmismatched-tags" #endif template -class tuple_size<::nlohmann::detail::view::view_item> // NOLINT(cert-dcl58-cpp) +class tuple_size<::nlohmann::detail::view::view_item> // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) : public std::integral_constant {}; template -class tuple_element> // NOLINT(cert-dcl58-cpp) +class tuple_element> // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) { public: using type = decltype(std::declval<::nlohmann::detail::view::view_item>().template get()); From 456735126178f77cfaec294922391e669f8821b9 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 01:24:40 +0200 Subject: [PATCH 30/34] Fix clang-tidy 22 findings in unit-json_view_edit.cpp and edit.hpp Signed-off-by: Niels Lohmann (cherry picked from commit cd435ea5d3fbd4f67ef32f1a635937d766973054) --- include/nlohmann/detail/view/edit.hpp | 4 ++-- tests/src/unit-json_view_edit.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/include/nlohmann/detail/view/edit.hpp b/include/nlohmann/detail/view/edit.hpp index 7a9daa6bc..fb8d769f3 100644 --- a/include/nlohmann/detail/view/edit.hpp +++ b/include/nlohmann/detail/view/edit.hpp @@ -323,7 +323,7 @@ class editor node* w = h + 1; std::size_t erased = 0; bool kept = false; - for (node* r = h + 1, *end = h + h->next; r != end; r += 2) + for (const node* r = h + 1, *end = h + h->next; r != end; r += 2) { const bool match = key_equals(*r, key); if (match && (kept || !keep_first)) @@ -547,7 +547,7 @@ class editor const bool negative = k == value_t::number_integer && static_cast(bits) < 0; std::uint64_t magnitude = negative ? 0 - bits : bits; std::array buf{}; - char* p = buf.data() + buf.size(); + char* p = buf.data() + buf.size(); // NOLINT(misc-const-correctness): digits are written through p do { *--p = static_cast('0' + (magnitude % 10)); diff --git a/tests/src/unit-json_view_edit.cpp b/tests/src/unit-json_view_edit.cpp index 29be6bb1b..215afd6aa 100644 --- a/tests/src/unit-json_view_edit.cpp +++ b/tests/src/unit-json_view_edit.cpp @@ -265,7 +265,7 @@ void check_lookups(const ordered_json_editable_view& v) } else if (v.is_array()) { - for (std::size_t i = 0; i < v.size(); ++i) + for (std::size_t i = 0; i < v.size(); ++i) // NOLINT(modernize-loop-convert): both v[i] and v.at(i) are tested { check_lookups(v[i]); check_lookups(v.at(i)); From 611faf2c8846e56f20743455623893b537edbce4 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 01:26:28 +0200 Subject: [PATCH 31/34] Fix clang-tidy 22 findings in unit-json_view_image.cpp and image.hpp Signed-off-by: Niels Lohmann --- include/nlohmann/detail/view/image.hpp | 10 +++++++++- tests/src/unit-json_view_image.cpp | 6 +++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/include/nlohmann/detail/view/image.hpp b/include/nlohmann/detail/view/image.hpp index d228034f7..91ff5acc8 100644 --- a/include/nlohmann/detail/view/image.hpp +++ b/include/nlohmann/detail/view/image.hpp @@ -437,7 +437,15 @@ inline bool check_float_ranges(std::vector& ranges, const unsigned { std::sort(ranges.begin(), ranges.end(), [](const float_range & a, const float_range & b) { - return a.off != b.off ? a.off < b.off : (a.len != b.len ? a.len < b.len : a.extra < b.extra); + if (a.off != b.off) + { + return a.off < b.off; + } + if (a.len != b.len) + { + return a.len < b.len; + } + return a.extra < b.extra; }); std::size_t end = 0; std::size_t i = 0; diff --git a/tests/src/unit-json_view_image.cpp b/tests/src/unit-json_view_image.cpp index 65d88f6ed..522a82be0 100644 --- a/tests/src/unit-json_view_image.cpp +++ b/tests/src/unit-json_view_image.cpp @@ -56,7 +56,7 @@ const char* const check_failed = "[json.exception.parse_error.116] parse error: std::string read_file(const std::string& name) { - std::ifstream f(std::string(TEST_DATA_DIRECTORY) + name, std::ios::binary); + const std::ifstream f(std::string(TEST_DATA_DIRECTORY) + name, std::ios::binary); std::stringstream ss; ss << f.rdbuf(); return ss.str(); @@ -332,7 +332,7 @@ TEST_CASE("json_view images: round trips") // exceed the probe limit, so the object has none and is searched // linearly; 40 of 200 keys in one slot (512 slots): a table with a // long chain - const struct + const struct // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays): an unnamed type { std::size_t slots; std::size_t colliding; @@ -349,7 +349,7 @@ TEST_CASE("json_view images: round trips") text.pop_back(); for (const std::string& key : duplicated) { - text += ",\"" + key + "\":\"last\""; + text += ",\"" + key + R"(":"last")"; } text += "}"; const json_document d = json_document::parse(text); From ccd2693623624d24e19b57edf96c77376af1d17e Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 01:26:31 +0200 Subject: [PATCH 32/34] Regenerate the amalgamated headers Signed-off-by: Niels Lohmann --- single_include/nlohmann/json_view.hpp | 34 +++++++++++++++++---------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index e00ae17c0..7e7e93de8 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -670,7 +670,7 @@ NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* vector_plain_run(const unsigned return p + (count_trailing_zeros(bits) >> 2u); } #else - const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); + const __m128i in = _mm_loadu_si128(static_cast(static_cast(p))); // NOLINT(bugprone-casting-through-void) const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), _mm_cmplt_epi8(in, _mm_set1_epi8(0x20))); const auto bits = static_cast(static_cast(_mm_movemask_epi8(special))); @@ -865,15 +865,15 @@ NLOHMANN_VIEW_SSSE3_TARGET NLOHMANN_VIEW_NOINLINE inline const unsigned char* sc #else // the same with SSSE3 (pshufb for the table lookups; nibbles from 16-bit // shifts, as there are no byte shifts) - const __m128i t1h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_high.data()))); - const __m128i t1l = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_low.data()))); - const __m128i t2h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_2_high.data()))); + const __m128i t1h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_high.data()))); // NOLINT(bugprone-casting-through-void) + const __m128i t1l = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_low.data()))); // NOLINT(bugprone-casting-through-void) + const __m128i t2h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_2_high.data()))); // NOLINT(bugprone-casting-through-void) const __m128i nibble = _mm_set1_epi8(0x0F); const __m128i zero = _mm_setzero_si128(); __m128i prev = zero; while (e - block >= 16) { - const __m128i in = _mm_loadu_si128(static_cast(static_cast(block))); + const __m128i in = _mm_loadu_si128(static_cast(static_cast(block))); // NOLINT(bugprone-casting-through-void) const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))), _mm_cmpeq_epi8(_mm_subs_epu8(in, _mm_set1_epi8(0x1F)), zero)); // in < 0x20 const __m128i prev1 = _mm_alignr_epi8(in, prev, 15); @@ -887,7 +887,7 @@ NLOHMANN_VIEW_SSSE3_TARGET NLOHMANN_VIEW_NOINLINE inline const unsigned char* sc const auto err_bits = ~static_cast(_mm_movemask_epi8(_mm_cmpeq_epi8(err, zero))) & 0xFFFFu; if (special_bits != 0) { - const unsigned k = static_cast(count_trailing_zeros(static_cast(special_bits))); + const auto k = static_cast(count_trailing_zeros(static_cast(special_bits))); if ((err_bits & ((2u << k) - 1u)) == 0) { return block + k; @@ -3190,7 +3190,7 @@ template SizeType to_index(IntegerType idx) noexcept { const IntegerType zero = 0; - const auto result = static_cast(idx); + const auto result = static_cast(idx); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): idx is an index, not a character return (idx < zero || static_cast(result) != idx) ? (std::numeric_limits::max)() : result; } @@ -3531,7 +3531,7 @@ class editor node* w = h + 1; std::size_t erased = 0; bool kept = false; - for (node* r = h + 1, *end = h + h->next; r != end; r += 2) + for (const node* r = h + 1, *end = h + h->next; r != end; r += 2) { const bool match = key_equals(*r, key); if (match && (kept || !keep_first)) @@ -3755,7 +3755,7 @@ class editor const bool negative = k == value_t::number_integer && static_cast(bits) < 0; std::uint64_t magnitude = negative ? 0 - bits : bits; std::array buf{}; - char* p = buf.data() + buf.size(); + char* p = buf.data() + buf.size(); // NOLINT(misc-const-correctness): digits are written through p do { *--p = static_cast('0' + (magnitude % 10)); @@ -4764,7 +4764,15 @@ inline bool check_float_ranges(std::vector& ranges, const unsigned { std::sort(ranges.begin(), ranges.end(), [](const float_range & a, const float_range & b) { - return a.off != b.off ? a.off < b.off : (a.len != b.len ? a.len < b.len : a.extra < b.extra); + if (a.off != b.off) + { + return a.off < b.off; + } + if (a.len != b.len) + { + return a.len < b.len; + } + return a.extra < b.extra; }); std::size_t end = 0; std::size_t i = 0; @@ -7129,7 +7137,7 @@ class basic_json_view // the end is unknown: assume a few bytes per node, the output buffer // grows should the value be larger const auto nodes = static_cast(document_data::after(m_node) - m_node); - return (std::min)(m_doc->size - m_node->off, static_cast(1024) + nodes * 16); + return (std::min)(m_doc->size - m_node->off, static_cast(1024) + (nodes * 16)); } /// the value of the first member with this key, or a discarded view @@ -7865,11 +7873,11 @@ namespace std // NOLINT(cert-dcl58-cpp) #pragma clang diagnostic ignored "-Wmismatched-tags" #endif template -class tuple_size<::nlohmann::detail::view::view_item> // NOLINT(cert-dcl58-cpp) +class tuple_size<::nlohmann::detail::view::view_item> // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) : public std::integral_constant {}; template -class tuple_element> // NOLINT(cert-dcl58-cpp) +class tuple_element> // NOLINT(cert-dcl58-cpp,bugprone-std-namespace-modification) { public: using type = decltype(std::declval<::nlohmann::detail::view::view_item>().template get()); From 525787a2f7266e3c19adecc49b19383e7a4e1565 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 01:46:20 +0200 Subject: [PATCH 33/34] Avoid GCC useless-cast and strict-overflow warnings in unit-json_view.cpp Signed-off-by: Niels Lohmann --- tests/src/unit-json_view.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp index 3dec8d676..e95158346 100644 --- a/tests/src/unit-json_view.cpp +++ b/tests/src/unit-json_view.cpp @@ -861,6 +861,8 @@ TEST_CASE("json_view element access and iteration") const json_document d = json_document::parse("[10,20,30]"); const json_view v = d.root(); const json j = v.materialize(); + // (a variable: a cast of a constant to int32_t is a useless cast to GCC) + const std::int32_t int32_index = 1; // (compile-time: no overload is ambiguous) CHECK(v[0].materialize() == 10); CHECK(v[1].materialize() == 20); @@ -876,7 +878,7 @@ TEST_CASE("json_view element access and iteration") CHECK(v[static_cast(2)].materialize() == 30); CHECK(v[std::int8_t(1)].materialize() == 20); CHECK(v[std::int16_t(2)].materialize() == 30); - CHECK(v[std::int32_t(1)].materialize() == 20); + CHECK(v[int32_index].materialize() == 20); CHECK(v[std::int64_t(2)].materialize() == 30); CHECK(v[std::uint32_t(0)].materialize() == 10); CHECK(v[std::uint64_t(1)].materialize() == 20); @@ -891,7 +893,7 @@ TEST_CASE("json_view element access and iteration") CHECK(v.at(2ULL).materialize() == 30); CHECK(v.at(static_cast(1)).materialize() == 20); CHECK(v.at(static_cast(2)).materialize() == 30); - CHECK(v.at(std::int32_t(0)).materialize() == 10); + CHECK(v.at(int32_index - 1).materialize() == 10); CHECK(v.at(std::uint32_t(0)).materialize() == 10); CHECK(v.at(std::int64_t(0)).materialize() == 10); CHECK(v.at(std::uint64_t(1)).materialize() == 20); @@ -1819,7 +1821,7 @@ TEST_CASE("json_view large objects") for (int object = 0; object < objects; ++object) { text += object != 0 ? ",{" : "{"; - for (int i = 0; i < members + object; ++i) + for (int i = 0, n = members + object; i < n; ++i) { text += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\":" + std::to_string(i); } @@ -1846,7 +1848,7 @@ TEST_CASE("json_view large objects") for (int object = 0; object < 5; ++object) { const json_view v = d.root()[static_cast(object)]; - for (int i = 0; i < 150 + object; ++i) + for (int i = 0, n = 150 + object; i < n; ++i) { CHECK(v[std::to_string(i)].get() == i); } From c2810ffe3d1504ae0214171338368ac05c092559 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 10 Oct 2026 02:11:49 +0200 Subject: [PATCH 34/34] Avoid GCC useless-cast warnings in the integer index tests Signed-off-by: Niels Lohmann --- tests/src/unit-json_view.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp index 3744b6f1a..256470b94 100644 --- a/tests/src/unit-json_view.cpp +++ b/tests/src/unit-json_view.cpp @@ -860,6 +860,8 @@ TEST_CASE("json_view element access and iteration") const json_document d = json_document::parse("[10,20,30]"); const json_view v = d.root(); const json j = v.materialize(); + // (a variable: a cast of a constant to int32_t is a useless cast to GCC) + const std::int32_t int32_index = 1; // (compile-time: no overload is ambiguous) CHECK(v[0].materialize() == 10); CHECK(v[1].materialize() == 20); @@ -875,7 +877,7 @@ TEST_CASE("json_view element access and iteration") CHECK(v[static_cast(2)].materialize() == 30); CHECK(v[std::int8_t(1)].materialize() == 20); CHECK(v[std::int16_t(2)].materialize() == 30); - CHECK(v[std::int32_t(1)].materialize() == 20); + CHECK(v[int32_index].materialize() == 20); CHECK(v[std::int64_t(2)].materialize() == 30); CHECK(v[std::uint32_t(0)].materialize() == 10); CHECK(v[std::uint64_t(1)].materialize() == 20); @@ -890,7 +892,7 @@ TEST_CASE("json_view element access and iteration") CHECK(v.at(2ULL).materialize() == 30); CHECK(v.at(static_cast(1)).materialize() == 20); CHECK(v.at(static_cast(2)).materialize() == 30); - CHECK(v.at(std::int32_t(0)).materialize() == 10); + CHECK(v.at(int32_index - 1).materialize() == 10); CHECK(v.at(std::uint32_t(0)).materialize() == 10); CHECK(v.at(std::int64_t(0)).materialize() == 10); CHECK(v.at(std::uint64_t(1)).materialize() == 20);