Description
Converting a std::optional<T> to JSON calls std::terminate() instead of propagating the exception when the conversion of the contained value throws. The overload is declared noexcept, but its body assigns *opt to the JSON value, which calls the user's to_json (or allocates):
include/nlohmann/detail/conversions/to_json.hpp#L290
void to_json(BasicJsonType& j, const std::optional<T>& opt) noexcept
{
if (opt.has_value())
{
j = *opt; // may throw: user to_json, std::bad_alloc for strings/arrays/objects
}
...
Anything that can throw inside j = *opt is affected: a user-defined to_json that reports an error by throwing, std::bad_alloc for std::optional<std::string>, std::optional<std::vector<...>>, or std::optional<json>, and exceptions from nested conversions (for example a type defined with NLOHMANN_DEFINE_TYPE_* that has a std::optional member whose value's to_json throws). The noexcept also propagates into the noexcept specification of basic_json's converting constructor, so the whole json j = opt; expression is noexcept(true).
Since when: the noexcept was added in #4742 (commit 96c1b52, 2025-04-15), which was merged after the v3.12.0 tag. The program below prints caught: cannot serialize Value with the v3.12.0 headers and terminates with develop, so no release is affected yet.
Open PR #5591 changes the body of this function (j = BasicJsonType(*opt);) but keeps the noexcept.
Possible fix: remove the noexcept (or make it conditional on the conversion of T being noexcept). I tested removing it: the program below then prints caught: cannot serialize Value. I did not run the full test suite with that change; no test in tests/src checks the noexcept-ness of this overload.
Reproduction steps
- Save the program below as
c01.cpp.
- Compile it against
develop: clang++ -std=c++17 -I include c01.cpp -o c01 (the same happens with -fsanitize=address,undefined and with GCC 16.2).
- Run
./c01.
Expected vs. actual results
- Expected: the
std::runtime_error thrown by to_json(json&, const Value&) propagates, and the program prints caught: cannot serialize Value.
- Actual:
std::terminate() is called and the process aborts.
Minimal code example
#include <nlohmann/json.hpp>
#include <iostream>
#include <optional>
#include <stdexcept>
using json = nlohmann::json;
struct Value {};
// a to_json that reports an error by throwing
void to_json(json&, const Value&)
{
throw std::runtime_error("cannot serialize Value");
}
int main()
{
std::optional<Value> opt = Value{};
try
{
json j = opt; // expected: the runtime_error propagates to the catch below
} // actual: std::terminate() is called
catch (const std::exception& e)
{
std::cout << "caught: " << e.what() << '\n';
}
}
Error messages
libc++abi: terminating due to uncaught exception of type std::runtime_error: cannot serialize Value
(process aborted, exit code 134)
With GCC 16.2 / libstdc++:
terminate called after throwing an instance of 'std::runtime_error'
what(): cannot serialize Value
(process aborted, exit code 134)
Compiler and operating system
Apple clang 21.0.0 (clang-2100.3.34.2), macOS 27.0 (arm64); also GCC 16.2 (/opt/homebrew/bin/g++-16)
Library version
develop @ 633de8e (not reproducible with 3.12.0, see above)
Validation
This issue was written by Claude Code on behalf of @nlohmann.
Description
Converting a
std::optional<T>to JSON callsstd::terminate()instead of propagating the exception when the conversion of the contained value throws. The overload is declarednoexcept, but its body assigns*optto the JSON value, which calls the user'sto_json(or allocates):include/nlohmann/detail/conversions/to_json.hpp#L290Anything that can throw inside
j = *optis affected: a user-definedto_jsonthat reports an error by throwing,std::bad_allocforstd::optional<std::string>,std::optional<std::vector<...>>, orstd::optional<json>, and exceptions from nested conversions (for example a type defined withNLOHMANN_DEFINE_TYPE_*that has astd::optionalmember whose value'sto_jsonthrows). Thenoexceptalso propagates into thenoexceptspecification ofbasic_json's converting constructor, so the wholejson j = opt;expression isnoexcept(true).Since when: the
noexceptwas added in #4742 (commit 96c1b52, 2025-04-15), which was merged after the v3.12.0 tag. The program below printscaught: cannot serialize Valuewith the v3.12.0 headers and terminates withdevelop, so no release is affected yet.Open PR #5591 changes the body of this function (
j = BasicJsonType(*opt);) but keeps thenoexcept.Possible fix: remove the
noexcept(or make it conditional on the conversion ofTbeingnoexcept). I tested removing it: the program below then printscaught: cannot serialize Value. I did not run the full test suite with that change; no test intests/srcchecks thenoexcept-ness of this overload.Reproduction steps
c01.cpp.develop:clang++ -std=c++17 -I include c01.cpp -o c01(the same happens with-fsanitize=address,undefinedand with GCC 16.2)../c01.Expected vs. actual results
std::runtime_errorthrown byto_json(json&, const Value&)propagates, and the program printscaught: cannot serialize Value.std::terminate()is called and the process aborts.Minimal code example
Error messages
With GCC 16.2 / libstdc++:
Compiler and operating system
Apple clang 21.0.0 (clang-2100.3.34.2), macOS 27.0 (arm64); also GCC 16.2 (/opt/homebrew/bin/g++-16)
Library version
develop@ 633de8e (not reproducible with 3.12.0, see above)Validation
developbranch is used.This issue was written by Claude Code on behalf of @nlohmann.