From 357a72e6e2d0d1cef79bb3a12499fab91286b819 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Tue, 29 Sep 2026 23:39:07 +0200 Subject: [PATCH] Give operator>> a strong exception-safety guarantee operator>> parsed directly into its basic_json& target, so a parse error left the target holding whatever was parsed before the error instead of its previous value. With JSON_DIAGNOSTICS=1, that partial value also violated the class invariant, because the parent pointers of an array or object's elements are only set when the container is closed, which a failed parse never reaches; copying such a value then aborted in assert_invariant(). Fix it the way basic_json::parse() already handles this: parse into a temporary and move it into the target only once parsing succeeds, so the target is left unchanged if an exception is thrown. Fixes #5652. Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/operator_gtgt.md | 6 ++++++ include/nlohmann/json.hpp | 5 ++++- single_include/nlohmann/json.hpp | 5 ++++- tests/src/unit-diagnostics.cpp | 17 +++++++++++++++++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/docs/mkdocs/docs/api/operator_gtgt.md b/docs/mkdocs/docs/api/operator_gtgt.md index 0173b9fb3..29fa31ee6 100644 --- a/docs/mkdocs/docs/api/operator_gtgt.md +++ b/docs/mkdocs/docs/api/operator_gtgt.md @@ -18,6 +18,10 @@ Deserializes an input stream to a JSON value. the stream `i` +## Exception safety + +Strong guarantee: if an exception is thrown, there are no changes in `j`. + ## Exceptions - Throws [`parse_error.101`](../home/exceptions.md#jsonexceptionparse_error101) in case of an unexpected token. @@ -118,3 +122,5 @@ being read. it as end of input; planned to become the default in version 4.0.0. - `JSON_PRECISE_STREAM_POSITION` added in version 3.13.0 to optionally leave the character that terminates a number in the stream; planned to become the default in version 4.0.0. +- Changed to the strong exception safety guarantee in version 3.13.0: `j` is no longer left with a partially parsed + value if parsing throws. diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 500fcddf2..997b4a551 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -5050,7 +5050,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_gtgt/ friend std::istream& operator>>(std::istream& i, basic_json& j) { - parser(detail::input_adapter(i)).parse(false, j); + // parse into a temporary so that j is left unchanged if parsing fails + basic_json result; + parser(detail::input_adapter(i)).parse(false, result); + j = std::move(result); return i; } #endif // JSON_NO_IO diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 576498738..67c6e495b 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -31131,7 +31131,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_gtgt/ friend std::istream& operator>>(std::istream& i, basic_json& j) { - parser(detail::input_adapter(i)).parse(false, j); + // parse into a temporary so that j is left unchanged if parsing fails + basic_json result; + parser(detail::input_adapter(i)).parse(false, result); + j = std::move(result); return i; } #endif // JSON_NO_IO diff --git a/tests/src/unit-diagnostics.cpp b/tests/src/unit-diagnostics.cpp index 3ae649e5b..c4f0f1423 100644 --- a/tests/src/unit-diagnostics.cpp +++ b/tests/src/unit-diagnostics.cpp @@ -17,6 +17,8 @@ #include using nlohmann::json; +#include + TEST_CASE("Better diagnostics") { SECTION("empty JSON Pointer") @@ -461,6 +463,21 @@ TEST_CASE("Regression tests for extended diagnostics") CHECK(copy == j); } } + + SECTION("Regression test for issue #5652 - operator>> leaves a partial value in its target on a parse error") + { + json j = "old value"; + std::istringstream is("[1, x"); + CHECK_THROWS_WITH_AS(is >> j, "[json.exception.parse_error.101] parse error at line 1, column 5: syntax error while parsing value - invalid literal; last read: '1, x'", json::parse_error); + + // j must be left unchanged, as json::parse() guarantees for its result + CHECK(j == "old value"); + + // copying j must not trigger assert_invariant(): a failed parse must + // not leave array/object elements without a parent pointer + json const copy = j; // NOLINT(performance-unnecessary-copy-initialization) + CHECK(copy == j); + } } TEST_CASE("Better diagnostics past the descent bound of update() and merge_patch()")