Compare commits

..
Author SHA1 Message Date
Niels Lohmann a05b22ddf3 Let to_json(std::optional<T>) 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<T>, so json j = opt; could
not report the error either.

Fixes #5642.
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-29 23:17:33 +02:00
6 changed files with 41 additions and 68 deletions
@@ -287,7 +287,7 @@ struct external_constructor<value_t::object>
#ifdef JSON_HAS_CPP_17
template<typename BasicJsonType, typename T,
enable_if_t<std::is_constructible<BasicJsonType, T>::value, int> = 0>
void to_json(BasicJsonType& j, const std::optional<T>& opt) noexcept
void to_json(BasicJsonType& j, const std::optional<T>& opt)
{
if (opt.has_value())
{
+8 -18
View File
@@ -678,29 +678,19 @@ class json_pointer
return nullptr;
}
// tokens that array_index() rejects with parse_error.106/109
// are passed on to it; all other tokens that it would reject
// with out_of_range.404/410 are detected here, so that this
// also works without exceptions
if (JSON_HEDLEY_UNLIKELY(reference_token.size() > 1 && !(reference_token[0] >= '1' && reference_token[0] <= '9')))
// may throw parse_error.106/109 for a malformed index; an
// index that is syntactically valid but cannot be
// represented (out_of_range.404/410) is treated like an
// out-of-range index below
typename BasicJsonType::size_type idx{};
JSON_TRY
{
static_cast<void>(array_index<BasicJsonType>(reference_token)); // throws parse_error.106/109
idx = array_index<BasicJsonType>(reference_token);
}
if (JSON_HEDLEY_UNLIKELY(reference_token.empty() || !std::all_of(reference_token.begin(), reference_token.end(), [](const char c)
{
return c >= '0' && c <= '9';
})))
JSON_INTERNAL_CATCH (detail::out_of_range&)
{
return nullptr;
}
errno = 0; // strtoull() does not reset errno on success
char* p_end = nullptr; // NOLINT(misc-const-correctness)
const unsigned long long magnitude = std::strtoull(reference_token.data(), &p_end, 10); // NOLINT(runtime/int)
if (JSON_HEDLEY_UNLIKELY(errno == ERANGE || magnitude >= static_cast<unsigned long long>((std::numeric_limits<typename BasicJsonType::size_type>::max)()))) // NOLINT(runtime/int)
{
return nullptr;
}
const auto idx = static_cast<typename BasicJsonType::size_type>(magnitude);
if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size()))
{
+9 -19
View File
@@ -6816,7 +6816,7 @@ struct external_constructor<value_t::object>
#ifdef JSON_HAS_CPP_17
template<typename BasicJsonType, typename T,
enable_if_t<std::is_constructible<BasicJsonType, T>::value, int> = 0>
void to_json(BasicJsonType& j, const std::optional<T>& opt) noexcept
void to_json(BasicJsonType& j, const std::optional<T>& opt)
{
if (opt.has_value())
{
@@ -19520,29 +19520,19 @@ class json_pointer
return nullptr;
}
// tokens that array_index() rejects with parse_error.106/109
// are passed on to it; all other tokens that it would reject
// with out_of_range.404/410 are detected here, so that this
// also works without exceptions
if (JSON_HEDLEY_UNLIKELY(reference_token.size() > 1 && !(reference_token[0] >= '1' && reference_token[0] <= '9')))
// may throw parse_error.106/109 for a malformed index; an
// index that is syntactically valid but cannot be
// represented (out_of_range.404/410) is treated like an
// out-of-range index below
typename BasicJsonType::size_type idx{};
JSON_TRY
{
static_cast<void>(array_index<BasicJsonType>(reference_token)); // throws parse_error.106/109
idx = array_index<BasicJsonType>(reference_token);
}
if (JSON_HEDLEY_UNLIKELY(reference_token.empty() || !std::all_of(reference_token.begin(), reference_token.end(), [](const char c)
{
return c >= '0' && c <= '9';
})))
JSON_INTERNAL_CATCH (detail::out_of_range&)
{
return nullptr;
}
errno = 0; // strtoull() does not reset errno on success
char* p_end = nullptr; // NOLINT(misc-const-correctness)
const unsigned long long magnitude = std::strtoull(reference_token.data(), &p_end, 10); // NOLINT(runtime/int)
if (JSON_HEDLEY_UNLIKELY(errno == ERANGE || magnitude >= static_cast<unsigned long long>((std::numeric_limits<typename BasicJsonType::size_type>::max)()))) // NOLINT(runtime/int)
{
return nullptr;
}
const auto idx = static_cast<typename BasicJsonType::size_type>(magnitude);
if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size()))
{
+23
View File
@@ -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<T> 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<std::string, std::optional<int>>(j_object) == opt_object);
}
#if !defined(JSON_NOEXCEPTION)
SECTION("exception from contained value's to_json propagates (#5642)")
{
// to_json(BasicJsonType&, const std::optional<T>&) 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<throwing_to_json_type> opt = throwing_to_json_type{};
CHECK_THROWS_WITH_AS(json(opt), "cannot serialize throwing_to_json_type", std::runtime_error&);
}
#endif
}
#endif
-15
View File
@@ -46,21 +46,6 @@ TEST_CASE("Tests with disabled exceptions")
CHECK(*sax_no_exception::error_string == "[json.exception.parse_error.101] parse error at line 1, column 1: syntax error while parsing value - invalid literal; last read: 'x'");
delete sax_no_exception::error_string; // NOLINT(cppcoreguidelines-owning-memory)
}
SECTION("issue #5672 - value(json_pointer, default) must not abort for array tokens that are not a valid index")
{
const json j = {1, 2, 3};
// a syntactically valid index that is out of range for this array
CHECK(j.value("/7"_json_pointer, 42) == 42);
// a reference token that is not a number at all
CHECK(j.value("/1a"_json_pointer, 42) == 42);
// the empty reference token (JSON pointer "/")
CHECK(j.value("/"_json_pointer, 42) == 42);
// an index whose magnitude does not fit into size_type
CHECK(j.value("/99999999999999999999999"_json_pointer, 42) == 42);
CHECK(j.value("/18446744073709551615"_json_pointer, 42) == 42);
}
}
DOCTEST_GCC_SUPPRESS_WARNING_POP
-15
View File
@@ -482,21 +482,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j
CHECK(j_array.value("/-"_json_pointer, 42) == 42);
CHECK(j_array_const.value("/-"_json_pointer, 42) == 42);
// Test an index with a non-digit after a valid leading digit; this is
// out_of_range (not parse_error) and must not throw (see #5672)
CHECK(j_array.value("/1a"_json_pointer, 42) == 42);
CHECK(j_array_const.value("/1a"_json_pointer, 42) == 42);
// Test the empty reference token (JSON pointer "/"); see #5672
CHECK(j_array.value("/"_json_pointer, 42) == 42);
CHECK(j_array_const.value("/"_json_pointer, 42) == 42);
// Test an index whose magnitude does not fit into size_type (see #5672)
CHECK(j_array.value("/99999999999999999999999"_json_pointer, 42) == 42);
CHECK(j_array_const.value("/99999999999999999999999"_json_pointer, 42) == 42);
CHECK(j_array.value("/18446744073709551615"_json_pointer, 42) == 42);
CHECK(j_array_const.value("/18446744073709551615"_json_pointer, 42) == 42);
#if !defined(JSON_NOEXCEPTION)
// Test malformed index (non-numeric) throws parse_error
CHECK_THROWS_WITH_AS(j_array.value("/foo"_json_pointer, 1), "[json.exception.parse_error.109] parse error: array index 'foo' is not a number", typename Json::parse_error&);