From 44a88d85bee2ca160ee732263eb0c9f313e5edbc Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 30 Sep 2026 20:07:29 +0200 Subject: [PATCH] Fix NLOHMANN_JSON_SERIALIZE_ENUM_STRICT's from_json message (#5698) from_json built its out_of_range.410 message with "..." + j.dump(). If the unmatched value is (or contains) a string with invalid UTF-8, that dump() itself throws type_error.316, so the caller got type_error.316 instead of the documented out_of_range.410; such strings can reach get() unvalidated, e.g. from from_cbor()/from_msgpack(). With a custom string_t, j.dump() returns that type, and "const char*" + string_t does not compile unless the type happens to provide operator+, so the macro failed to compile for such types. Build the message with detail::concat(), which appends any type exposing data()/size() and always yields a std::string, and dump with error_handler_t::replace so building the message itself cannot throw. Added regression tests: an invalid-UTF-8 case in the existing strict-enum test in unit-conversions.cpp, and a strict-enum use with alt_string (the custom string_t from unit-alt-string.cpp) to cover the compile failure. Fixes #5667. Signed-off-by: Niels Lohmann --- include/nlohmann/detail/macro_scope.hpp | 2 +- single_include/nlohmann/json.hpp | 2 +- tests/src/unit-alt-string.cpp | 24 ++++++++++++++++++++++++ tests/src/unit-conversions.cpp | 6 ++++++ 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/include/nlohmann/detail/macro_scope.hpp b/include/nlohmann/detail/macro_scope.hpp index 3a4eb79f4..9c35cc3b7 100644 --- a/include/nlohmann/detail/macro_scope.hpp +++ b/include/nlohmann/detail/macro_scope.hpp @@ -336,7 +336,7 @@ void templated_json_throw(ExceptionType exception) return ej_pair.second == j; \ }); \ if (it != std::end(m)) e = it->first; \ - else templated_json_throw(nlohmann::detail::out_of_range::create(410,"enum value out of range for " #ENUM_TYPE ": " + j.dump(), &j)); \ + else templated_json_throw(nlohmann::detail::out_of_range::create(410, nlohmann::detail::concat("enum value out of range for " #ENUM_TYPE ": ", j.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace)), &j)); \ } // Ugly macros to avoid uglier copy-paste when specializing basic_json. They diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index f1aad02ce..16c2cdca6 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -2749,7 +2749,7 @@ void templated_json_throw(ExceptionType exception) return ej_pair.second == j; \ }); \ if (it != std::end(m)) e = it->first; \ - else templated_json_throw(nlohmann::detail::out_of_range::create(410,"enum value out of range for " #ENUM_TYPE ": " + j.dump(), &j)); \ + else templated_json_throw(nlohmann::detail::out_of_range::create(410, nlohmann::detail::concat("enum value out of range for " #ENUM_TYPE ": ", j.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace)), &j)); \ } // Ugly macros to avoid uglier copy-paste when specializing basic_json. They diff --git a/tests/src/unit-alt-string.cpp b/tests/src/unit-alt-string.cpp index cddaadb7e..f95d2213c 100644 --- a/tests/src/unit-alt-string.cpp +++ b/tests/src/unit-alt-string.cpp @@ -174,6 +174,15 @@ bool operator<(const char* op1, const alt_string& op2) noexcept return op1 < op2.str_impl; } +enum class alt_color { red, green }; + +// NOLINTNEXTLINE(misc-use-internal-linkage,misc-const-correctness,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) - false positive +NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(alt_color, +{ + {alt_color::red, "red"}, + {alt_color::green, "green"}, +}) + TEST_CASE("alternative string type") { SECTION("binary formats") @@ -374,4 +383,19 @@ TEST_CASE("alternative string type") const auto j2 = j.flatten(); CHECK(j2.dump() == R"({"/foo/0":"bar","/foo/1":"baz"})"); } + + SECTION("strict enum") + { + // regression test for #5667: NLOHMANN_JSON_SERIALIZE_ENUM_STRICT's from_json + // built its exception message with "..." + j.dump(), which does not compile + // when j.dump() returns a custom string_t (here alt_string) instead of + // std::string + alt_json doc; + doc = "red"; + CHECK(doc.get() == alt_color::red); + + alt_json _; + doc = "blue"; + CHECK_THROWS_WITH_AS(_ = doc.get(), "[json.exception.out_of_range.410] enum value out of range for alt_color: \"blue\"", alt_json::out_of_range&); + } } diff --git a/tests/src/unit-conversions.cpp b/tests/src/unit-conversions.cpp index 0d53f2226..2fce40fa9 100644 --- a/tests/src/unit-conversions.cpp +++ b/tests/src/unit-conversions.cpp @@ -1748,6 +1748,12 @@ TEST_CASE("Strict JSON to enum mapping") // conversion of unmapped enum -> exception thrown CHECK_THROWS_WITH_AS(json(strict_cards::andere), "[json.exception.out_of_range.410] enum value out of range for strict_cards", json::out_of_range&); + + // invalid UTF-8 -> out_of_range.410, not the type_error.316 thrown while building the + // message (regression test for #5667); such strings can reach get() unvalidated, + // e.g. from from_cbor()/from_msgpack() (#5529) + const json j_invalid_utf8 = "\xFF"; + CHECK_THROWS_WITH_AS(_ = j_invalid_utf8.get(), "[json.exception.out_of_range.410] enum value out of range for strict_cards: \"\xEF\xBF\xBD\"", json::out_of_range&); } SECTION("traditional enum")