From b1bb9fce0c9ae9db5cecf8e3bd78519ac70683ee Mon Sep 17 00:00:00 2001 From: Kirill Lokotkov Date: Fri, 15 May 2026 20:25:16 +0300 Subject: [PATCH] Fix for printing long doubles bug in dump_float (#3929) --- include/nlohmann/detail/output/serializer.hpp | 2 +- single_include/nlohmann/json.hpp | 2 +- tests/src/unit-serialization.cpp | 71 +++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/include/nlohmann/detail/output/serializer.hpp b/include/nlohmann/detail/output/serializer.hpp index c1d50740a..25a64d648 100644 --- a/include/nlohmann/detail/output/serializer.hpp +++ b/include/nlohmann/detail/output/serializer.hpp @@ -836,7 +836,7 @@ class serializer static int snprintf_float(char* buf, std::size_t size, int d, long double x) { // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg) - return (std::snprintf)(buf, size, "%.*lg", d, x); + return (std::snprintf)(buf, size, "%.*Lg", d, x); } void dump_float(number_float_t x, std::false_type /*is_ieee_single_or_double*/) diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index e3e701557..aa9dc9c73 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -19828,7 +19828,7 @@ class serializer static int snprintf_float(char* buf, std::size_t size, int d, long double x) { // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg) - return (std::snprintf)(buf, size, "%.*lg", d, x); + return (std::snprintf)(buf, size, "%.*Lg", d, x); } void dump_float(number_float_t x, std::false_type /*is_ieee_single_or_double*/) diff --git a/tests/src/unit-serialization.cpp b/tests/src/unit-serialization.cpp index 5c2ab8148..925c1e78c 100644 --- a/tests/src/unit-serialization.cpp +++ b/tests/src/unit-serialization.cpp @@ -11,6 +11,7 @@ #include using nlohmann::json; +#include #include #include @@ -295,3 +296,73 @@ TEST_CASE("dump with binary values") "]"); } } + +TEST_CASE("dump for basic_json with long double number_float_t") +{ + // Custom basic_json instantiation with long double as NumberFloatType. + // On platforms where long double is wider than double (e.g. GCC/Clang on + // Linux/macOS x86_64), dump() goes through the snprintf path in + // serializer::dump_float(x, std::false_type). That branch must use the + // "%.*Lg" format specifier; using "%.*g" with a long double argument is + // undefined behavior and corrupts the output. + using long_double_json = nlohmann::basic_json; + + SECTION("round-trip dump/parse") + { + constexpr std::array values = + {{ + 0.0L, -0.0L, 1.0L, -1.0L, + 0.5L, -0.5L, 1.5L, -2.25L, + 1.23e45L, 1.23e-45L, + (std::numeric_limits::min)(), + std::numeric_limits::lowest(), + (std::numeric_limits::max)() + }}; + + for (long double v : values) + { + const long_double_json j = v; + const auto s = j.dump(); + const auto j2 = long_double_json::parse(s); + CHECK(j2.template get() == v); + } + } + + SECTION("exact dump string for simple values") + { + CHECK(long_double_json(0.5L).dump() == "0.5"); + CHECK(long_double_json(-0.5L).dump() == "-0.5"); + CHECK(long_double_json(1.5L).dump() == "1.5"); + CHECK(long_double_json(-2.25L).dump() == "-2.25"); + CHECK(long_double_json(0.0L).dump() == "0.0"); + CHECK(long_double_json(1.0L).dump() == "1.0"); + CHECK(long_double_json(-1.0L).dump() == "-1.0"); + CHECK(long_double_json(100.0L).dump() == "100.0"); + } + + SECTION("NaN and infinity dump as null") + { + CHECK(long_double_json(std::numeric_limits::quiet_NaN()).dump() == "null"); + CHECK(long_double_json(std::numeric_limits::infinity()).dump() == "null"); + CHECK(long_double_json(-std::numeric_limits::infinity()).dump() == "null"); + } + + SECTION("dump output matches double for exactly-representable values") + { + auto check_same = [](long double v_ld, double v_d) + { + const long_double_json j_ld = v_ld; + const json j_d = v_d; + CHECK(j_ld.dump() == j_d.dump()); + }; + + check_same(0.0L, 0.0); + check_same(0.5L, 0.5); + check_same(-0.5L, -0.5); + check_same(1.5L, 1.5); + check_same(-2.25L, -2.25); + check_same(1.0L, 1.0); + check_same(100.0L, 100.0); + } +}