From a05b22ddf3fe9d6a6658313b64071b5f25576ef2 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Tue, 29 Sep 2026 23:17:33 +0200 Subject: [PATCH] Let to_json(std::optional) propagate exceptions from T's to_json The overload was marked noexcept even though its body assigns *opt to the JSON value, which calls T's to_json (or allocates for std::string, std::vector, or json). Any exception from there -- a user-defined to_json reporting an error, or std::bad_alloc -- called std::terminate() instead of propagating. The noexcept also made basic_json's converting constructor noexcept(true) for std::optional, so json j = opt; could not report the error either. Fixes #5642. Signed-off-by: Niels Lohmann --- .../nlohmann/detail/conversions/to_json.hpp | 2 +- single_include/nlohmann/json.hpp | 2 +- tests/src/unit-conversions.cpp | 23 +++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/include/nlohmann/detail/conversions/to_json.hpp b/include/nlohmann/detail/conversions/to_json.hpp index 491bb9873..d77381249 100644 --- a/include/nlohmann/detail/conversions/to_json.hpp +++ b/include/nlohmann/detail/conversions/to_json.hpp @@ -287,7 +287,7 @@ struct external_constructor #ifdef JSON_HAS_CPP_17 template::value, int> = 0> -void to_json(BasicJsonType& j, const std::optional& opt) noexcept +void to_json(BasicJsonType& j, const std::optional& opt) { if (opt.has_value()) { diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 576498738..83b6f10be 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -6816,7 +6816,7 @@ struct external_constructor #ifdef JSON_HAS_CPP_17 template::value, int> = 0> -void to_json(BasicJsonType& j, const std::optional& opt) noexcept +void to_json(BasicJsonType& j, const std::optional& opt) { if (opt.has_value()) { diff --git a/tests/src/unit-conversions.cpp b/tests/src/unit-conversions.cpp index 90d972f71..6e0c73b48 100644 --- a/tests/src/unit-conversions.cpp +++ b/tests/src/unit-conversions.cpp @@ -1826,6 +1826,16 @@ TEST_CASE("std::u8string") #endif #endif +// a type whose to_json reports an error by throwing, used below to check that +// converting a std::optional to JSON propagates an exception thrown while +// converting its contained value instead of calling std::terminate (#5642) +struct throwing_to_json_type {}; + +void to_json(json&, const throwing_to_json_type&) +{ + throw std::runtime_error("cannot serialize throwing_to_json_type"); +} + TEST_CASE("std::optional") { SECTION("null") @@ -1908,6 +1918,19 @@ TEST_CASE("std::optional") CHECK(json(opt_object) == j_object); CHECK(std::map>(j_object) == opt_object); } + +#if !defined(JSON_NOEXCEPTION) + SECTION("exception from contained value's to_json propagates (#5642)") + { + // to_json(BasicJsonType&, const std::optional&) must not be + // noexcept: it calls T's to_json, which may throw (a user-defined + // to_json that reports an error, or std::bad_alloc for T = + // std::string/vector/json). Before the fix, this called + // std::terminate() instead of letting the exception propagate. + const std::optional opt = throwing_to_json_type{}; + CHECK_THROWS_WITH_AS(json(opt), "cannot serialize throwing_to_json_type", std::runtime_error&); + } +#endif } #endif