mirror of
https://github.com/nlohmann/json.git
synced 2026-07-02 08:44:19 +00:00
🐛 avoid assertion in patch (#5222)
This commit is contained in:
@@ -32,6 +32,8 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va
|
||||
could not be resolved successfully in the current JSON value; example: `"key baz not found"`.
|
||||
- Throws [`out_of_range.405`](../../home/exceptions.md#jsonexceptionout_of_range405) if JSON pointer has no parent
|
||||
("add", "remove", "move")
|
||||
- Throws [`out_of_range.411`](../../home/exceptions.md#jsonexceptionout_of_range411) if an "add" operation's target
|
||||
location has a parent that is neither an object nor an array.
|
||||
- Throws [`other_error.501`](../../home/exceptions.md#jsonexceptionother_error501) if "test" operation was
|
||||
unsuccessful.
|
||||
|
||||
@@ -71,3 +73,5 @@ is thrown. In any case, the original value is not changed: the patch is applied
|
||||
## Version history
|
||||
|
||||
- Added in version 2.0.0.
|
||||
- Added [`out_of_range.411`](../../home/exceptions.md#jsonexceptionout_of_range411) and stopped relying on an internal assertion when an "add" operation's
|
||||
target location has a non-object/non-array parent in version 3.12.x.
|
||||
|
||||
@@ -28,6 +28,8 @@ No guarantees, value may be corrupted by an unsuccessful patch operation.
|
||||
could not be resolved successfully in the current JSON value; example: `"key baz not found"`.
|
||||
- Throws [`out_of_range.405`](../../home/exceptions.md#jsonexceptionout_of_range405) if JSON pointer has no parent
|
||||
("add", "remove", "move")
|
||||
- Throws [`out_of_range.411`](../../home/exceptions.md#jsonexceptionout_of_range411) if an "add" operation's target
|
||||
location has a parent that is neither an object nor an array.
|
||||
- Throws [`other_error.501`](../../home/exceptions.md#jsonexceptionother_error501) if "test" operation was
|
||||
unsuccessful.
|
||||
|
||||
@@ -68,3 +70,5 @@ function throws an exception.
|
||||
## Version history
|
||||
|
||||
- Added in version 3.11.0.
|
||||
- Added [`out_of_range.411`](../../home/exceptions.md#jsonexceptionout_of_range411) and stopped relying on an internal assertion when an "add" operation's
|
||||
target location has a non-object/non-array parent in version 3.12.x.
|
||||
|
||||
@@ -881,6 +881,20 @@ a JSON pointer exceeds the range of `size_type` (e.g., on 32-bit platforms).
|
||||
array index 18446744073709551616 exceeds size_type
|
||||
```
|
||||
|
||||
### json.exception.out_of_range.411
|
||||
|
||||
A JSON Patch `add` operation cannot be applied because the target location's parent is neither an object nor an array. Per [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902), an `add` target must reference a member of an existing object or an element of an existing array; a primitive value (string, number, boolean, etc.) cannot receive a new member or element.
|
||||
|
||||
!!! failure "Example message"
|
||||
|
||||
```
|
||||
cannot add value: the JSON Patch 'add' target's parent is of type string, but must be an object or array
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
This exception was added in version 3.12.x. Before that, this situation hit an internal assertion (aborting the program in debug builds) or was silently ignored when assertions were disabled.
|
||||
|
||||
## Further exceptions
|
||||
|
||||
This exception is thrown in case of errors that cannot be classified with the
|
||||
|
||||
+11
-10
@@ -4890,16 +4890,17 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
break;
|
||||
}
|
||||
|
||||
// if there exists a parent, it cannot be primitive
|
||||
case value_t::string: // LCOV_EXCL_LINE
|
||||
case value_t::boolean: // LCOV_EXCL_LINE
|
||||
case value_t::number_integer: // LCOV_EXCL_LINE
|
||||
case value_t::number_unsigned: // LCOV_EXCL_LINE
|
||||
case value_t::number_float: // LCOV_EXCL_LINE
|
||||
case value_t::binary: // LCOV_EXCL_LINE
|
||||
case value_t::discarded: // LCOV_EXCL_LINE
|
||||
default: // LCOV_EXCL_LINE
|
||||
JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE
|
||||
// the parent of an "add" target must be an object or array
|
||||
// (see #4292)
|
||||
case value_t::string:
|
||||
case value_t::boolean:
|
||||
case value_t::number_integer:
|
||||
case value_t::number_unsigned:
|
||||
case value_t::number_float:
|
||||
case value_t::binary:
|
||||
case value_t::discarded:
|
||||
default:
|
||||
JSON_THROW(out_of_range::create(411, detail::concat("cannot add value: the JSON Patch 'add' target's parent is of type ", parent.type_name(), ", but must be an object or array"), &parent));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -25408,16 +25408,17 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
break;
|
||||
}
|
||||
|
||||
// if there exists a parent, it cannot be primitive
|
||||
case value_t::string: // LCOV_EXCL_LINE
|
||||
case value_t::boolean: // LCOV_EXCL_LINE
|
||||
case value_t::number_integer: // LCOV_EXCL_LINE
|
||||
case value_t::number_unsigned: // LCOV_EXCL_LINE
|
||||
case value_t::number_float: // LCOV_EXCL_LINE
|
||||
case value_t::binary: // LCOV_EXCL_LINE
|
||||
case value_t::discarded: // LCOV_EXCL_LINE
|
||||
default: // LCOV_EXCL_LINE
|
||||
JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE
|
||||
// the parent of an "add" target must be an object or array
|
||||
// (see #4292)
|
||||
case value_t::string:
|
||||
case value_t::boolean:
|
||||
case value_t::number_integer:
|
||||
case value_t::number_unsigned:
|
||||
case value_t::number_float:
|
||||
case value_t::binary:
|
||||
case value_t::discarded:
|
||||
default:
|
||||
JSON_THROW(out_of_range::create(411, detail::concat("cannot add value: the JSON Patch 'add' target's parent is of type ", parent.type_name(), ", but must be an object or array"), &parent));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -37,4 +37,14 @@ TEST_CASE("Better diagnostics with positions")
|
||||
CHECK_THROWS_WITH_AS(j.get<int>(),
|
||||
"[json.exception.type_error.302] type must be number, but is string", json::type_error);
|
||||
}
|
||||
|
||||
SECTION("JSON patch add to primitive parent (#4292)")
|
||||
{
|
||||
// the JSON Patch "add" target /foo/bar/baz has a string parent
|
||||
// (/foo/bar); the position of that parent is reported in the message
|
||||
const json doc = json::parse(R"({"foo":{"bar":"a string"}})");
|
||||
const json patch = json::parse(R"([{"op":"add","path":"/foo/bar/baz","value":1}])");
|
||||
CHECK_THROWS_WITH_AS(doc.patch(patch),
|
||||
"[json.exception.out_of_range.411] (/foo/bar) (bytes 14-24) cannot add value: the JSON Patch 'add' target's parent is of type string, but must be an object or array", json::out_of_range);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1334,3 +1334,57 @@ TEST_CASE("JSON patch")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("JSON patch - add to a primitive parent (regression #4292)")
|
||||
{
|
||||
// Regression test for https://github.com/nlohmann/json/issues/4292
|
||||
//
|
||||
// An "add" operation whose parent location resolves to a primitive
|
||||
// (non-container) value must be rejected with a catchable exception.
|
||||
// Previously this hit JSON_ASSERT(false) in operation_add, which aborts
|
||||
// the process in debug builds and silently dropped the operation (leaving
|
||||
// a wrong result) when assertions were compiled out (NDEBUG). It now
|
||||
// throws out_of_range.411.
|
||||
//
|
||||
// The documents below are constructed programmatically (not parsed) so
|
||||
// they carry no byte positions; the JSON_DIAGNOSTICS path prefix is
|
||||
// handled by the guards. The exact message with positions is covered in
|
||||
// unit-diagnostic-positions.cpp.
|
||||
|
||||
SECTION("string parent")
|
||||
{
|
||||
json const doc = {{"foo", {{"bar", "a string"}}}};
|
||||
json const patch = {{{"op", "add"}, {"path", "/foo/bar/baz"}, {"value", 1}}};
|
||||
#if JSON_DIAGNOSTICS
|
||||
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.411] (/foo/bar) cannot add value: the JSON Patch 'add' target's parent is of type string, but must be an object or array", json::out_of_range&);
|
||||
#else
|
||||
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.411] cannot add value: the JSON Patch 'add' target's parent is of type string, but must be an object or array", json::out_of_range&);
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("number parent")
|
||||
{
|
||||
json const doc = {{"foo", 1}};
|
||||
json const patch = {{{"op", "add"}, {"path", "/foo/bar"}, {"value", 2}}};
|
||||
#if JSON_DIAGNOSTICS
|
||||
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.411] (/foo) cannot add value: the JSON Patch 'add' target's parent is of type number, but must be an object or array", json::out_of_range&);
|
||||
#else
|
||||
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.411] cannot add value: the JSON Patch 'add' target's parent is of type number, but must be an object or array", json::out_of_range&);
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("original two-step sequence from the issue")
|
||||
{
|
||||
// The user's two-step patch from #4292: first turn /xyz/1 into a
|
||||
// string, then try to add a member inside that string.
|
||||
json const doc = R"( { "xyz": [ { "lmn": "214", "nnp": "001" } ] } )"_json;
|
||||
json const patch = R"(
|
||||
[
|
||||
{ "op": "add", "path": "/xyz/1", "value": "" },
|
||||
{ "op": "add", "path": "/xyz/1/lmn", "value": "214" }
|
||||
]
|
||||
)"_json;
|
||||
|
||||
CHECK_THROWS_AS(doc.patch(patch), json::out_of_range&);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user