diff --git a/docs/mkdocs/docs/api/operator_gtgt.md b/docs/mkdocs/docs/api/operator_gtgt.md index b68889af9..35b8c1016 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, or if @@ -125,3 +129,5 @@ being read. the stream; planned to become the default in version 4.0.0. - Fixed a null pointer dereference for an `std::istream` without a stream buffer (now throws `parse_error.101`), and a crash (`std::terminate`) when `i` has `eofbit` in its exception mask, in version 3.13.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 cb08777bc..78c4e4f62 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -5092,7 +5092,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 88c29a4f1..5ef598150 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -32019,7 +32019,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 46f41f252..b3778e802 100644 --- a/tests/src/unit-diagnostics.cpp +++ b/tests/src/unit-diagnostics.cpp @@ -19,6 +19,7 @@ using nlohmann::json; #include #include +#include TEST_CASE("Better diagnostics") { @@ -492,6 +493,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()")