From 50dc14c12cec75d1886e304c64069a525cd3300a Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sun, 11 Oct 2026 13:37:11 +0200 Subject: [PATCH] Format the out_of_range.407 number without dump() dump() instantiates the serializer for the whole value type, which does not compile for object key types it cannot write (e.g., the custom_key_test::key_c_str of #5795), even though the value is always a number here. Produce the decimal digits directly instead; they are generated per digit so that the minimum of a signed type is never negated as a whole. Signed-off-by: Niels Lohmann --- .../nlohmann/detail/output/binary_writer.hpp | 55 ++++++++++++++++++- single_include/nlohmann/json.hpp | 55 ++++++++++++++++++- tests/src/unit-custom-number-types.cpp | 5 ++ 3 files changed, 109 insertions(+), 6 deletions(-) diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index f5faacc79..3ab316c10 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -911,15 +911,64 @@ class binary_writer return integer_fits_min(n) && integer_fits_max(n); } + /*! + @brief the decimal digits of the non-negative integer @a n + */ + template + static std::string integer_to_string(NumberType n, std::false_type /*is_signed*/) + { + std::string result; + do + { + result.push_back(static_cast('0' + n % 10)); + n = static_cast(n / 10); + } + while (n != 0); + std::reverse(result.begin(), result.end()); + return result; + } + + /*! + @brief the decimal digits of the integer @a n, with a leading '-' if it + is negative + */ + template + static std::string integer_to_string(NumberType n, std::true_type /*is_signed*/) + { + // the remainder of a negative n is not positive; each digit is + // negated on its own, because negating the minimum of NumberType + // as a whole would overflow + const bool negative = n < 0; + std::string result; + do + { + const auto remainder = n % 10; + result.push_back(static_cast('0' + (remainder < 0 ? -remainder : remainder))); + n = static_cast(n / 10); + } + while (n != 0); + if (negative) + { + result.push_back('-'); + } + std::reverse(result.begin(), result.end()); + return result; + } + /*! @brief throws because the integer @a j is too large for @a format_name @throw out_of_range.407 always */ JSON_HEDLEY_NO_RETURN static void throw_integer_out_of_range(const BasicJsonType& j, const char* format_name, const char* range) { - // dump() rather than std::to_string(), which has no overload for - // integer types wider than 64 bits - const auto number = j.dump(); + // neither std::to_string(), which has no overload for integer types + // wider than 64 bits, nor dump(), which would also instantiate the + // serializer for objects and fail for key types it cannot write + using integer_limits = std::numeric_limits; + using unsigned_limits = std::numeric_limits; + const std::string number = j.is_number_unsigned() + ? integer_to_string(j.m_data.m_value.number_unsigned, std::integral_constant()) + : integer_to_string(j.m_data.m_value.number_integer, std::integral_constant()); static_cast(number); // unused when JSON_NOEXCEPTION is defined static_cast(format_name); static_cast(range); diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 4657087f9..f26b6683f 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -22646,15 +22646,64 @@ class binary_writer return integer_fits_min(n) && integer_fits_max(n); } + /*! + @brief the decimal digits of the non-negative integer @a n + */ + template + static std::string integer_to_string(NumberType n, std::false_type /*is_signed*/) + { + std::string result; + do + { + result.push_back(static_cast('0' + n % 10)); + n = static_cast(n / 10); + } + while (n != 0); + std::reverse(result.begin(), result.end()); + return result; + } + + /*! + @brief the decimal digits of the integer @a n, with a leading '-' if it + is negative + */ + template + static std::string integer_to_string(NumberType n, std::true_type /*is_signed*/) + { + // the remainder of a negative n is not positive; each digit is + // negated on its own, because negating the minimum of NumberType + // as a whole would overflow + const bool negative = n < 0; + std::string result; + do + { + const auto remainder = n % 10; + result.push_back(static_cast('0' + (remainder < 0 ? -remainder : remainder))); + n = static_cast(n / 10); + } + while (n != 0); + if (negative) + { + result.push_back('-'); + } + std::reverse(result.begin(), result.end()); + return result; + } + /*! @brief throws because the integer @a j is too large for @a format_name @throw out_of_range.407 always */ JSON_HEDLEY_NO_RETURN static void throw_integer_out_of_range(const BasicJsonType& j, const char* format_name, const char* range) { - // dump() rather than std::to_string(), which has no overload for - // integer types wider than 64 bits - const auto number = j.dump(); + // neither std::to_string(), which has no overload for integer types + // wider than 64 bits, nor dump(), which would also instantiate the + // serializer for objects and fail for key types it cannot write + using integer_limits = std::numeric_limits; + using unsigned_limits = std::numeric_limits; + const std::string number = j.is_number_unsigned() + ? integer_to_string(j.m_data.m_value.number_unsigned, std::integral_constant()) + : integer_to_string(j.m_data.m_value.number_integer, std::integral_constant()); static_cast(number); // unused when JSON_NOEXCEPTION is defined static_cast(format_name); static_cast(range); diff --git a/tests/src/unit-custom-number-types.cpp b/tests/src/unit-custom-number-types.cpp index e5d9d7b30..787e36557 100644 --- a/tests/src/unit-custom-number-types.cpp +++ b/tests/src/unit-custom-number-types.cpp @@ -183,6 +183,11 @@ TEST_CASE("custom number types: integers beyond 64 bits") CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(int_big), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by CBOR as it does not fit [-2^64, 2^64-1]", out_of_range&); CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(int_big_negative), "[json.exception.out_of_range.407] integer number -1267650600228229401496703205376 cannot be represented by CBOR as it does not fit [-2^64, 2^64-1]", out_of_range&); CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(uint_big), "[json.exception.out_of_range.407] integer number 1267650600228229401496703205376 cannot be represented by CBOR as it does not fit uint64", out_of_range&); + // the extremes of the 128-bit types, whose digits are produced without + // negating the minimum as a whole + CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(make_int((std::numeric_limits::min)())), "[json.exception.out_of_range.407] integer number -170141183460469231731687303715884105728 cannot be represented by CBOR as it does not fit [-2^64, 2^64-1]", out_of_range&); + CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(make_int((std::numeric_limits::max)())), "[json.exception.out_of_range.407] integer number 170141183460469231731687303715884105727 cannot be represented by CBOR as it does not fit [-2^64, 2^64-1]", out_of_range&); + CHECK_THROWS_WITH_AS(_ = wide_json::to_cbor(make_uint((std::numeric_limits::max)())), "[json.exception.out_of_range.407] integer number 340282366920938463463374607431768211455 cannot be represented by CBOR as it does not fit uint64", out_of_range&); CHECK_THROWS_AS(_ = wide_json::to_cbor(make_int(two_64)), out_of_range&); CHECK_THROWS_AS(_ = wide_json::to_cbor(make_int(-two_64 - 1)), out_of_range&); CHECK_THROWS_AS(_ = wide_json::to_cbor(make_uint(static_cast(max_uint64) + 1)), out_of_range&);