Document std::optional<T> direct-init/copy-init limitation with null

Add regression test pinning current behavior (CHECK_THROWS_AS) in the null
section of unit-conversions.cpp with detailed comment explaining the C++
language-level cause (std::optional's own converting constructor wins
overload resolution over basic_json::operator T()).

Add a warning callout in conversions.md documenting that direct construction/
assignment of std::optional<T> from JSON null throws type_error 302, with a
clear workaround (use get<std::optional<T>>() or get_to() instead, which
correctly produce std::nullopt).

This is a limitation at the language level: there is no SFINAE path to
distinguish "called from inside std::optional's own constructor" from "direct
call", so fixing it would require breaking changes to operator ValueType().
A permanent fix belongs in the 4.0 type-strictness redesign (#3453).

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Niels Lohmann
2026-07-08 15:07:50 +02:00
parent 3b0dd69928
commit 2f383b80ee
2 changed files with 28 additions and 0 deletions
+10
View File
@@ -1771,6 +1771,16 @@ TEST_CASE("std::optional")
CHECK(json(opt_null) == j_null);
CHECK(j_null.get<std::optional<std::string>>() == std::nullopt);
// Constructing std::optional<T> directly from JSON null throws because
// std::optional's own converting constructor is chosen over basic_json's
// operator T(). This is a language-level limitation (std::optional<T> is
// constructible from T, and T is constructible from basic_json via the
// operator); there is no SFINAE path that distinguishes "call from inside
// std::optional's constructor" from "direct call". Use get<std::optional<T>>()
// or get_to() instead for correct null handling. See json#4864 and #XXXX.
CHECK_THROWS_AS(std::optional<std::string>(j_null), json::type_error);
CHECK_THROWS_AS(std::optional<int>(j_null), json::type_error);
}
SECTION("string")