From 2f383b80ee262b71cc25a5024a6fbf7325876295 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 8 Jul 2026 15:07:50 +0200 Subject: [PATCH] Document std::optional 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 from JSON null throws type_error 302, with a clear workaround (use get>() 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 Co-Authored-By: Claude Code --- docs/mkdocs/docs/features/conversions.md | 18 ++++++++++++++++++ tests/src/unit-conversions.cpp | 10 ++++++++++ 2 files changed, 28 insertions(+) diff --git a/docs/mkdocs/docs/features/conversions.md b/docs/mkdocs/docs/features/conversions.md index eaa213324..7f63587fd 100644 --- a/docs/mkdocs/docs/features/conversions.md +++ b/docs/mkdocs/docs/features/conversions.md @@ -66,6 +66,24 @@ which forces the explicit `get` form and can catch unintended conversions at com floating-point value as an integer truncates it, and narrowing conversions may overflow. See [number conversion](types/number_handling.md#number-conversion) for details and how to guard against it. +!!! warning "std::optional direct construction from JSON null throws" + + Constructing or assigning `std::optional` directly from a JSON value does not correctly produce + `std::nullopt` for a JSON `null`: + + ```cpp + json j_null; + std::optional opt = j_null; // ❌ throws type_error 302 + ``` + + This is due to C++ language rules: `std::optional` has its own converting constructor that is chosen over + `basic_json::operator T()` when both are viable. Use `get>()` or `get_to()` instead: + + ```cpp + auto opt = j_null.get>(); // ✅ std::nullopt + j_null.get_to(opt); // ✅ std::nullopt + ``` + ## Putting values in The reverse direction works the same way: assigning or constructing a `json` from a C++ value converts it to JSON. diff --git a/tests/src/unit-conversions.cpp b/tests/src/unit-conversions.cpp index 7542ef630..0aac264ab 100644 --- a/tests/src/unit-conversions.cpp +++ b/tests/src/unit-conversions.cpp @@ -1771,6 +1771,16 @@ TEST_CASE("std::optional") CHECK(json(opt_null) == j_null); CHECK(j_null.get>() == std::nullopt); + + // Constructing std::optional 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 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>() + // or get_to() instead for correct null handling. See json#4864 and #XXXX. + CHECK_THROWS_AS(std::optional(j_null), json::type_error); + CHECK_THROWS_AS(std::optional(j_null), json::type_error); } SECTION("string")