mirror of
https://github.com/nlohmann/json.git
synced 2026-09-09 09:47:58 +00:00
* Reject JSON Patch move when from is a proper prefix of path RFC 6902 (section 4.4) forbids "from" from being a proper prefix of "path" for a "move" operation: "a location cannot be moved into one of its children." "move" is implemented as remove-then-add with no check for this. For object targets, the subsequent "add" happened to throw as a side effect of resolving through the now-removed parent, but for array targets, removing the "from" element shifts subsequent indices, so "path" silently re-resolves to a different element and the operation "succeeds" with a silently corrupted document. Add a check, before performing the remove/add, for whether "from" is a proper prefix of "path" at the reference-token level. This compares json_pointer's already-unescaped reference_tokens vectors (basic_json is a friend of json_pointer) rather than the raw pointer strings, so that tokens containing escaped '/' or '~' characters are compared correctly, and a token that merely looks like a string prefix (e.g. "/ab" vs "/abc/x") is not mistaken for a pointer-token prefix. When "from" is a proper prefix of "path", throw out_of_range.414. Fixes #5397. Stacked on top of the fix for #5396 (branch issue-5396-patch-remove-primitive-parent), since both touch the same patch_inplace move/remove handling in include/nlohmann/json.hpp. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Add root-pointer and array-append-token edge case tests for the move prefix check Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Replace std::equal with an explicitly-bounded loop in the move prefix check The three-iterator std::equal(first1, last1, first2) form has no explicit end iterator for the second range, which a static analyzer (Flawfinder, CWE-126) flags as a potential over-read even though the preceding size comparison already guarantees the second range is long enough. Rather than argue the point, make the bound visible in the code itself via an explicit loop -- every access to ptr.reference_tokens is now guarded by the same index the loop condition bounds against from_size. (The C++14 four-iterator std::equal(first1, last1, first2, last2) form was tried first as a more minimal fix, but this codebase targets C++11 and that overload is not safely usable under -std=c++11 with all supported standard library implementations.) Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Extract the move prefix check into a named helper lambda Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Account for JSON_DIAGNOSTIC_POSITIONS in the move-prefix-check error messages out_of_range::create() includes a "(bytes X-Y)" position annotation when JSON_DIAGNOSTIC_POSITIONS is enabled, which the ci_test_diagnostic_positions CI job builds the whole suite with. The five new out_of_range.414 assertions only checked the annotation-free message. Confirmed JSON_DIAGNOSTICS produces the same (annotation-free) message as the default build for this particular throw site (its path-based annotation is empty at the root, where &result always points here), so only two message variants are needed, not three. Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me>
1658 lines
62 KiB
C++
1658 lines
62 KiB
C++
// __ _____ _____ _____
|
|
// __| | __| | | | JSON for Modern C++ (supporting code)
|
|
// | | |__ | | | | | | version 3.12.0
|
|
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
|
//
|
|
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#include "doctest_compatibility.h"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
using nlohmann::json;
|
|
#ifdef JSON_TEST_NO_GLOBAL_UDLS
|
|
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
|
|
#endif
|
|
|
|
#include <fstream>
|
|
#include "make_test_data_available.hpp"
|
|
|
|
TEST_CASE("JSON patch")
|
|
{
|
|
SECTION("examples from RFC 6902")
|
|
{
|
|
SECTION("4. Operations")
|
|
{
|
|
// the ordering of members in JSON objects is not significant:
|
|
const json op1 = R"({ "op": "add", "path": "/a/b/c", "value": "foo" })"_json;
|
|
const json op2 = R"({ "path": "/a/b/c", "op": "add", "value": "foo" })"_json;
|
|
const json op3 = R"({ "value": "foo", "path": "/a/b/c", "op": "add" })"_json;
|
|
|
|
// check if the operation objects are equivalent
|
|
CHECK(op1 == op2);
|
|
CHECK(op1 == op3);
|
|
}
|
|
|
|
SECTION("4.1 add")
|
|
{
|
|
json const patch1 = R"([{ "op": "add", "path": "/a/b", "value": [ "foo", "bar" ] }])"_json;
|
|
|
|
// However, the object itself or an array containing it does need
|
|
// to exist, and it remains an error for that not to be the case.
|
|
// For example, an "add" with a target location of "/a/b" starting
|
|
// with this document
|
|
json const doc1 = R"({ "a": { "foo": 1 } })"_json;
|
|
|
|
// is not an error, because "a" exists, and "b" will be added to
|
|
// its value.
|
|
CHECK_NOTHROW(doc1.patch(patch1));
|
|
auto doc1_ans = R"(
|
|
{
|
|
"a": {
|
|
"foo": 1,
|
|
"b": [ "foo", "bar" ]
|
|
}
|
|
}
|
|
)"_json;
|
|
CHECK(doc1.patch(patch1) == doc1_ans);
|
|
|
|
// It is an error in this document:
|
|
json const doc2 = R"({ "q": { "bar": 2 } })"_json;
|
|
|
|
// because "a" does not exist.
|
|
#if JSON_DIAGNOSTIC_POSITIONS
|
|
CHECK_THROWS_WITH_AS(doc2.patch(patch1), "[json.exception.out_of_range.403] (bytes 0-21) key 'a' not found", json::out_of_range&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(doc2.patch(patch1), "[json.exception.out_of_range.403] key 'a' not found", json::out_of_range&);
|
|
#endif
|
|
|
|
json const doc3 = R"({ "a": {} })"_json;
|
|
json const patch2 = R"([{ "op": "add", "path": "/a/b/c", "value": 1 }])"_json;
|
|
|
|
// should cause an error because "b" does not exist in doc3
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(doc3.patch(patch2), "[json.exception.out_of_range.403] (/a) key 'b' not found", json::out_of_range&);
|
|
#elif JSON_DIAGNOSTIC_POSITIONS
|
|
CHECK_THROWS_WITH_AS(doc3.patch(patch2), "[json.exception.out_of_range.403] (bytes 7-9) key 'b' not found", json::out_of_range&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(doc3.patch(patch2), "[json.exception.out_of_range.403] key 'b' not found", json::out_of_range&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("4.2 remove")
|
|
{
|
|
// If removing an element from an array, any elements above the
|
|
// specified index are shifted one position to the left.
|
|
json const doc = {1, 2, 3, 4};
|
|
json const patch = {{{"op", "remove"}, {"path", "/1"}}};
|
|
CHECK(doc.patch(patch) == json({1, 3, 4}));
|
|
}
|
|
|
|
SECTION("A.1. Adding an Object Member")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{ "foo": "bar"}
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "add", "path": "/baz", "value": "qux" }
|
|
]
|
|
)"_json;
|
|
|
|
// The resulting JSON document:
|
|
json expected = R"(
|
|
{
|
|
"baz": "qux",
|
|
"foo": "bar"
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("A.2. Adding an Array Element")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{ "foo": [ "bar", "baz" ] }
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "add", "path": "/foo/1", "value": "qux" }
|
|
]
|
|
)"_json;
|
|
|
|
// The resulting JSON document:
|
|
json expected = R"(
|
|
{ "foo": [ "bar", "qux", "baz" ] }
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("A.3. Removing an Object Member")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{
|
|
"baz": "qux",
|
|
"foo": "bar"
|
|
}
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "remove", "path": "/baz" }
|
|
]
|
|
)"_json;
|
|
|
|
// The resulting JSON document:
|
|
json expected = R"(
|
|
{ "foo": "bar" }
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("A.4. Removing an Array Element")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{ "foo": [ "bar", "qux", "baz" ] }
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "remove", "path": "/foo/1" }
|
|
]
|
|
)"_json;
|
|
|
|
// The resulting JSON document:
|
|
json expected = R"(
|
|
{ "foo": [ "bar", "baz" ] }
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("A.5. Replacing a Value")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{
|
|
"baz": "qux",
|
|
"foo": "bar"
|
|
}
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "replace", "path": "/baz", "value": "boo" }
|
|
]
|
|
)"_json;
|
|
|
|
json expected = R"(
|
|
{
|
|
"baz": "boo",
|
|
"foo": "bar"
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("A.6. Moving a Value")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{
|
|
"foo": {
|
|
"bar": "baz",
|
|
"waldo": "fred"
|
|
},
|
|
"qux": {
|
|
"corge": "grault"
|
|
}
|
|
}
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "move", "from": "/foo/waldo", "path": "/qux/thud" }
|
|
]
|
|
)"_json;
|
|
|
|
// The resulting JSON document:
|
|
json expected = R"(
|
|
{
|
|
"foo": {
|
|
"bar": "baz"
|
|
},
|
|
"qux": {
|
|
"corge": "grault",
|
|
"thud": "fred"
|
|
}
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("A.7. Moving a Value")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{ "foo": [ "all", "grass", "cows", "eat" ] }
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "move", "from": "/foo/1", "path": "/foo/3" }
|
|
]
|
|
)"_json;
|
|
|
|
// The resulting JSON document:
|
|
json expected = R"(
|
|
{ "foo": [ "all", "cows", "eat", "grass" ] }
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("A.8. Testing a Value: Success")
|
|
{
|
|
// An example target JSON document:
|
|
json doc = R"(
|
|
{
|
|
"baz": "qux",
|
|
"foo": [ "a", 2, "c" ]
|
|
}
|
|
)"_json;
|
|
|
|
// A JSON Patch document that will result in successful evaluation:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "test", "path": "/baz", "value": "qux" },
|
|
{ "op": "test", "path": "/foo/1", "value": 2 }
|
|
]
|
|
)"_json;
|
|
|
|
// check if evaluation does not throw
|
|
CHECK_NOTHROW(doc.patch(patch));
|
|
// check if patched document is unchanged
|
|
CHECK(doc.patch(patch) == doc);
|
|
}
|
|
|
|
SECTION("A.9. Testing a Value: Error")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{ "baz": "qux" }
|
|
)"_json;
|
|
|
|
// A JSON Patch document that will result in an error condition:
|
|
json patch = R"(
|
|
[
|
|
{ "op": "test", "path": "/baz", "value": "bar" }
|
|
]
|
|
)"_json;
|
|
|
|
// check that evaluation throws
|
|
CHECK_THROWS_AS(doc.patch(patch), json::other_error&);
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (/0) unsuccessful: " + patch[0].dump());
|
|
#elif JSON_DIAGNOSTIC_POSITIONS
|
|
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (bytes 47-95) unsuccessful: " + patch[0].dump());
|
|
#else
|
|
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] unsuccessful: " + patch[0].dump());
|
|
#endif
|
|
}
|
|
|
|
SECTION("A.10. Adding a Nested Member Object")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{ "foo": "bar" }
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "add", "path": "/child", "value": { "grandchild": { } } }
|
|
]
|
|
)"_json;
|
|
|
|
// The resulting JSON document:
|
|
json expected = R"(
|
|
{
|
|
"foo": "bar",
|
|
"child": {
|
|
"grandchild": {
|
|
}
|
|
}
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("A.11. Ignoring Unrecognized Elements")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{ "foo": "bar" }
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "add", "path": "/baz", "value": "qux", "xyz": 123 }
|
|
]
|
|
)"_json;
|
|
|
|
json expected = R"(
|
|
{
|
|
"foo": "bar",
|
|
"baz": "qux"
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("A.12. Adding to a Nonexistent Target")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{ "foo": "bar" }
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "add", "path": "/baz/bat", "value": "qux" }
|
|
]
|
|
)"_json;
|
|
|
|
// This JSON Patch document, applied to the target JSON document
|
|
// above, would result in an error (therefore, it would not be
|
|
// applied), because the "add" operation's target location that
|
|
// references neither the root of the document, nor a member of
|
|
// an existing object, nor a member of an existing array.
|
|
#if JSON_DIAGNOSTIC_POSITIONS
|
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.403] (bytes 21-37) key 'baz' not found", json::out_of_range&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.403] key 'baz' not found", json::out_of_range&);
|
|
#endif
|
|
}
|
|
|
|
// A.13. Invalid JSON Patch Document
|
|
// not applicable
|
|
|
|
SECTION("A.14. Escape Ordering")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{
|
|
"/": 9,
|
|
"~1": 10
|
|
}
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{"op": "test", "path": "/~01", "value": 10}
|
|
]
|
|
)"_json;
|
|
|
|
json expected = R"(
|
|
{
|
|
"/": 9,
|
|
"~1": 10
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("A.15. Comparing Strings and Numbers")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{
|
|
"/": 9,
|
|
"~1": 10
|
|
}
|
|
)"_json;
|
|
|
|
// A JSON Patch document that will result in an error condition:
|
|
json patch = R"(
|
|
[
|
|
{"op": "test", "path": "/~01", "value": "10"}
|
|
]
|
|
)"_json;
|
|
|
|
// check that evaluation throws
|
|
CHECK_THROWS_AS(doc.patch(patch), json::other_error&);
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (/0) unsuccessful: " + patch[0].dump());
|
|
#elif JSON_DIAGNOSTIC_POSITIONS
|
|
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (bytes 47-92) unsuccessful: " + patch[0].dump());
|
|
#else
|
|
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] unsuccessful: " + patch[0].dump());
|
|
#endif
|
|
}
|
|
|
|
SECTION("A.16. Adding an Array Value")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{ "foo": ["bar"] }
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "add", "path": "/foo/-", "value": ["abc", "def"] }
|
|
]
|
|
)"_json;
|
|
|
|
// The resulting JSON document:
|
|
json expected = R"(
|
|
{ "foo": ["bar", ["abc", "def"]] }
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
}
|
|
|
|
SECTION("own examples")
|
|
{
|
|
SECTION("add")
|
|
{
|
|
SECTION("add to the root element")
|
|
{
|
|
// If the path is the root of the target document - the
|
|
// specified value becomes the entire content of the target
|
|
// document.
|
|
|
|
// An example target JSON document:
|
|
json const doc = 17;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "add", "path": "", "value": [1,2,3] }
|
|
]
|
|
)"_json;
|
|
|
|
// The resulting JSON document:
|
|
json expected = {1, 2, 3};
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("add to end of the array")
|
|
{
|
|
// The specified index MUST NOT be greater than the number of
|
|
// elements in the array. The example below uses and index of
|
|
// exactly the number of elements in the array which is legal.
|
|
|
|
// An example target JSON document:
|
|
json const doc = {0, 1, 2};
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "add", "path": "/3", "value": 3 }
|
|
]
|
|
)"_json;
|
|
|
|
// The resulting JSON document:
|
|
json expected = {0, 1, 2, 3};
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
}
|
|
|
|
SECTION("copy")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{
|
|
"foo": {
|
|
"bar": "baz",
|
|
"waldo": "fred"
|
|
},
|
|
"qux": {
|
|
"corge": "grault"
|
|
}
|
|
}
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "copy", "from": "/foo/waldo", "path": "/qux/thud" }
|
|
]
|
|
)"_json;
|
|
|
|
// The resulting JSON document:
|
|
json expected = R"(
|
|
{
|
|
"foo": {
|
|
"bar": "baz",
|
|
"waldo": "fred"
|
|
},
|
|
"qux": {
|
|
"corge": "grault",
|
|
"thud": "fred"
|
|
}
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("replace")
|
|
{
|
|
json const j = "string";
|
|
json const patch = {{{"op", "replace"}, {"path", ""}, {"value", 1}}};
|
|
CHECK(j.patch(patch) == json(1));
|
|
}
|
|
|
|
SECTION("documentation GIF")
|
|
{
|
|
{
|
|
// a JSON patch
|
|
json const p1 = R"(
|
|
[{"op": "add", "path": "/GB", "value": "London"}]
|
|
)"_json;
|
|
|
|
// a JSON value
|
|
json const source = R"(
|
|
{"D": "Berlin", "F": "Paris"}
|
|
)"_json;
|
|
|
|
// apply the patch
|
|
const json target = source.patch(p1);
|
|
// target = { "D": "Berlin", "F": "Paris", "GB": "London" }
|
|
CHECK(target == R"({ "D": "Berlin", "F": "Paris", "GB": "London" })"_json);
|
|
|
|
// create a diff from two JSONs
|
|
const json p2 = json::diff(target, source); // NOLINT(readability-suspicious-call-argument)
|
|
// p2 = [{"op": "delete", "path": "/GB"}]
|
|
CHECK(p2 == R"([{"op":"remove","path":"/GB"}])"_json);
|
|
}
|
|
{
|
|
// a JSON value
|
|
json j = {"good", "bad", "ugly"};
|
|
|
|
// a JSON pointer
|
|
auto ptr = json::json_pointer("/2");
|
|
|
|
// use to access elements
|
|
j[ptr] = {{"it", "cattivo"}};
|
|
CHECK(j == R"(["good","bad",{"it":"cattivo"}])"_json);
|
|
|
|
// use user-defined string literal
|
|
j["/2/en"_json_pointer] = "ugly";
|
|
CHECK(j == R"(["good","bad",{"en":"ugly","it":"cattivo"}])"_json);
|
|
|
|
const json flat = j.flatten();
|
|
CHECK(flat == R"({"/0":"good","/1":"bad","/2/en":"ugly","/2/it":"cattivo"})"_json);
|
|
}
|
|
}
|
|
}
|
|
|
|
SECTION("errors")
|
|
{
|
|
SECTION("unknown operation")
|
|
{
|
|
SECTION("not an array")
|
|
{
|
|
json const j;
|
|
json const patch = {{"op", "add"}, {"path", ""}, {"value", 1}};
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.104] parse error: JSON patch must be an array of objects", json::parse_error&);
|
|
}
|
|
|
|
SECTION("not an array of objects")
|
|
{
|
|
json const j;
|
|
json const patch = {"op", "add", "path", "", "value", 1};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.104] parse error: (/0) JSON patch must be an array of objects", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.104] parse error: JSON patch must be an array of objects", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("missing 'op'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"foo", "bar"}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation must have member 'op'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation must have member 'op'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("non-string 'op'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", 1}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation must have string member 'op'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation must have string member 'op'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("invalid operation")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "foo"}, {"path", ""}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation value 'foo' is invalid", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation value 'foo' is invalid", json::parse_error&);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
SECTION("add")
|
|
{
|
|
SECTION("missing 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "add"}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'add' must have member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'add' must have member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("non-string 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "add"}, {"path", 1}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'add' must have string member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'add' must have string member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("missing 'value'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "add"}, {"path", ""}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'add' must have member 'value'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'add' must have member 'value'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("invalid array index")
|
|
{
|
|
json const j = {1, 2};
|
|
json const patch = {{{"op", "add"}, {"path", "/4"}, {"value", 4}}};
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.out_of_range.401] array index 4 is out of range", json::out_of_range&);
|
|
}
|
|
}
|
|
|
|
SECTION("remove")
|
|
{
|
|
SECTION("missing 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "remove"}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'remove' must have member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'remove' must have member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("non-string 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "remove"}, {"path", 1}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'remove' must have string member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'remove' must have string member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("nonexisting target location (array)")
|
|
{
|
|
json const j = {1, 2, 3};
|
|
json const patch = {{{"op", "remove"}, {"path", "/17"}}};
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.out_of_range.401] array index 17 is out of range", json::out_of_range&);
|
|
}
|
|
|
|
SECTION("nonexisting target location (object)")
|
|
{
|
|
json const j = {{"foo", 1}, {"bar", 2}};
|
|
json const patch = {{{"op", "remove"}, {"path", "/baz"}}};
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.out_of_range.403] key 'baz' not found", json::out_of_range&);
|
|
}
|
|
|
|
SECTION("root element as target location")
|
|
{
|
|
json const j = "string";
|
|
json const patch = {{{"op", "remove"}, {"path", ""}}};
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.out_of_range.405] JSON pointer has no parent", json::out_of_range&);
|
|
}
|
|
}
|
|
|
|
SECTION("replace")
|
|
{
|
|
SECTION("missing 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "replace"}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'replace' must have member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'replace' must have member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("non-string 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "replace"}, {"path", 1}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'replace' must have string member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'replace' must have string member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("missing 'value'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "replace"}, {"path", ""}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'replace' must have member 'value'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'replace' must have member 'value'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("nonexisting target location (array)")
|
|
{
|
|
json const j = {1, 2, 3};
|
|
json const patch = {{{"op", "replace"}, {"path", "/17"}, {"value", 19}}};
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.out_of_range.401] array index 17 is out of range", json::out_of_range&);
|
|
}
|
|
|
|
SECTION("nonexisting target location (object)")
|
|
{
|
|
json const j = {{"foo", 1}, {"bar", 2}};
|
|
json const patch = {{{"op", "replace"}, {"path", "/baz"}, {"value", 3}}};
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.out_of_range.403] key 'baz' not found", json::out_of_range&);
|
|
}
|
|
}
|
|
|
|
SECTION("move")
|
|
{
|
|
SECTION("missing 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "move"}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'move' must have member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'move' must have member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("non-string 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "move"}, {"path", 1}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'move' must have string member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'move' must have string member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("missing 'from'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "move"}, {"path", ""}}};
|
|
CHECK_THROWS_AS(j.patch(patch), json::parse_error&);
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'move' must have member 'from'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'move' must have member 'from'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("non-string 'from'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "move"}, {"path", ""}, {"from", 1}}};
|
|
CHECK_THROWS_AS(j.patch(patch), json::parse_error&);
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'move' must have string member 'from'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'move' must have string member 'from'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("nonexisting from location (array)")
|
|
{
|
|
json const j = {1, 2, 3};
|
|
json const patch = {{{"op", "move"}, {"path", "/0"}, {"from", "/5"}}};
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.out_of_range.401] array index 5 is out of range", json::out_of_range&);
|
|
}
|
|
|
|
SECTION("nonexisting from location (object)")
|
|
{
|
|
json const j = {{"foo", 1}, {"bar", 2}};
|
|
json const patch = {{{"op", "move"}, {"path", "/baz"}, {"from", "/baz"}}};
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.out_of_range.403] key 'baz' not found", json::out_of_range&);
|
|
}
|
|
}
|
|
|
|
SECTION("copy")
|
|
{
|
|
SECTION("missing 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "copy"}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'copy' must have member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'copy' must have member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("non-string 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "copy"}, {"path", 1}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'copy' must have string member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'copy' must have string member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("missing 'from'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "copy"}, {"path", ""}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'copy' must have member 'from'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'copy' must have member 'from'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("non-string 'from'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "copy"}, {"path", ""}, {"from", 1}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'copy' must have string member 'from'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'copy' must have string member 'from'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("nonexisting from location (array)")
|
|
{
|
|
json const j = {1, 2, 3};
|
|
json const patch = {{{"op", "copy"}, {"path", "/0"}, {"from", "/5"}}};
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.out_of_range.401] array index 5 is out of range", json::out_of_range&);
|
|
}
|
|
|
|
SECTION("nonexisting from location (object)")
|
|
{
|
|
json const j = {{"foo", 1}, {"bar", 2}};
|
|
json const patch = {{{"op", "copy"}, {"path", "/fob"}, {"from", "/baz"}}};
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.out_of_range.403] key 'baz' not found", json::out_of_range&);
|
|
}
|
|
}
|
|
|
|
SECTION("test")
|
|
{
|
|
SECTION("missing 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "test"}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'test' must have member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'test' must have member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("non-string 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "test"}, {"path", 1}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'test' must have string member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'test' must have string member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("missing 'value'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "test"}, {"path", ""}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'test' must have member 'value'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'test' must have member 'value'", json::parse_error&);
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
SECTION("Examples from jsonpatch.com")
|
|
{
|
|
SECTION("Simple Example")
|
|
{
|
|
// The original document
|
|
json const doc = R"(
|
|
{
|
|
"baz": "qux",
|
|
"foo": "bar"
|
|
}
|
|
)"_json;
|
|
|
|
// The patch
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "replace", "path": "/baz", "value": "boo" },
|
|
{ "op": "add", "path": "/hello", "value": ["world"] },
|
|
{ "op": "remove", "path": "/foo"}
|
|
]
|
|
)"_json;
|
|
|
|
// The result
|
|
json result = R"(
|
|
{
|
|
"baz": "boo",
|
|
"hello": ["world"]
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == result);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, result)) == result);
|
|
}
|
|
|
|
SECTION("Operations")
|
|
{
|
|
// The original document
|
|
json const doc = R"(
|
|
{
|
|
"biscuits": [
|
|
{"name":"Digestive"},
|
|
{"name": "Choco Liebniz"}
|
|
]
|
|
}
|
|
)"_json;
|
|
|
|
SECTION("add")
|
|
{
|
|
// The patch
|
|
json const patch = R"(
|
|
[
|
|
{"op": "add", "path": "/biscuits/1", "value": {"name": "Ginger Nut"}}
|
|
]
|
|
)"_json;
|
|
|
|
// The result
|
|
json result = R"(
|
|
{
|
|
"biscuits": [
|
|
{"name": "Digestive"},
|
|
{"name": "Ginger Nut"},
|
|
{"name": "Choco Liebniz"}
|
|
]
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == result);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, result)) == result);
|
|
}
|
|
|
|
SECTION("remove")
|
|
{
|
|
// The patch
|
|
json const patch = R"(
|
|
[
|
|
{"op": "remove", "path": "/biscuits"}
|
|
]
|
|
)"_json;
|
|
|
|
// The result
|
|
json result = R"(
|
|
{}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == result);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, result)) == result);
|
|
}
|
|
|
|
SECTION("replace")
|
|
{
|
|
// The patch
|
|
json const patch = R"(
|
|
[
|
|
{"op": "replace", "path": "/biscuits/0/name", "value": "Chocolate Digestive"}
|
|
]
|
|
)"_json;
|
|
|
|
// The result
|
|
json result = R"(
|
|
{
|
|
"biscuits": [
|
|
{"name": "Chocolate Digestive"},
|
|
{"name": "Choco Liebniz"}
|
|
]
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == result);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, result)) == result);
|
|
}
|
|
|
|
SECTION("copy")
|
|
{
|
|
// The patch
|
|
json const patch = R"(
|
|
[
|
|
{"op": "copy", "from": "/biscuits/0", "path": "/best_biscuit"}
|
|
]
|
|
)"_json;
|
|
|
|
// The result
|
|
json result = R"(
|
|
{
|
|
"biscuits": [
|
|
{"name": "Digestive"},
|
|
{"name": "Choco Liebniz"}
|
|
],
|
|
"best_biscuit": {
|
|
"name": "Digestive"
|
|
}
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == result);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, result)) == result);
|
|
}
|
|
|
|
SECTION("move")
|
|
{
|
|
// The patch
|
|
json const patch = R"(
|
|
[
|
|
{"op": "move", "from": "/biscuits", "path": "/cookies"}
|
|
]
|
|
)"_json;
|
|
|
|
// The result
|
|
json result = R"(
|
|
{
|
|
"cookies": [
|
|
{"name": "Digestive"},
|
|
{"name": "Choco Liebniz"}
|
|
]
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == result);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, result)) == result);
|
|
}
|
|
|
|
SECTION("test")
|
|
{
|
|
// The patch
|
|
json patch = R"(
|
|
[
|
|
{"op": "test", "path": "/best_biscuit/name", "value": "Choco Liebniz"}
|
|
]
|
|
)"_json;
|
|
|
|
// the test will fail
|
|
CHECK_THROWS_AS(doc.patch(patch), json::other_error&);
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (/0) unsuccessful: " + patch[0].dump());
|
|
#elif JSON_DIAGNOSTIC_POSITIONS
|
|
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (bytes 47-117) unsuccessful: " + patch[0].dump());
|
|
#else
|
|
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] unsuccessful: " + patch[0].dump());
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
SECTION("Examples from bruth.github.io/jsonpatch-js")
|
|
{
|
|
SECTION("add")
|
|
{
|
|
CHECK(R"( {} )"_json.patch(
|
|
R"( [{"op": "add", "path": "/foo", "value": "bar"}] )"_json
|
|
) == R"( {"foo": "bar"} )"_json);
|
|
|
|
CHECK(R"( {"foo": [1, 3]} )"_json.patch(
|
|
R"( [{"op": "add", "path": "/foo", "value": "bar"}] )"_json
|
|
) == R"( {"foo": "bar"} )"_json);
|
|
|
|
CHECK(R"( {"foo": [{}]} )"_json.patch(
|
|
R"( [{"op": "add", "path": "/foo/0/bar", "value": "baz"}] )"_json
|
|
) == R"( {"foo": [{"bar": "baz"}]} )"_json);
|
|
}
|
|
|
|
SECTION("remove")
|
|
{
|
|
CHECK(R"( {"foo": "bar"} )"_json.patch(
|
|
R"( [{"op": "remove", "path": "/foo"}] )"_json
|
|
) == R"( {} )"_json);
|
|
|
|
CHECK(R"( {"foo": [1, 2, 3]} )"_json.patch(
|
|
R"( [{"op": "remove", "path": "/foo/1"}] )"_json
|
|
) == R"( {"foo": [1, 3]} )"_json);
|
|
|
|
CHECK(R"( {"foo": [{"bar": "baz"}]} )"_json.patch(
|
|
R"( [{"op": "remove", "path": "/foo/0/bar"}] )"_json
|
|
) == R"( {"foo": [{}]} )"_json);
|
|
}
|
|
|
|
SECTION("replace")
|
|
{
|
|
CHECK(R"( {"foo": "bar"} )"_json.patch(
|
|
R"( [{"op": "replace", "path": "/foo", "value": 1}] )"_json
|
|
) == R"( {"foo": 1} )"_json);
|
|
|
|
CHECK(R"( {"foo": [1, 2, 3]} )"_json.patch(
|
|
R"( [{"op": "replace", "path": "/foo/1", "value": 4}] )"_json
|
|
) == R"( {"foo": [1, 4, 3]} )"_json);
|
|
|
|
CHECK(R"( {"foo": [{"bar": "baz"}]} )"_json.patch(
|
|
R"( [{"op": "replace", "path": "/foo/0/bar", "value": 1}] )"_json
|
|
) == R"( {"foo": [{"bar": 1}]} )"_json);
|
|
}
|
|
|
|
SECTION("move")
|
|
{
|
|
CHECK(R"( {"foo": [1, 2, 3]} )"_json.patch(
|
|
R"( [{"op": "move", "from": "/foo", "path": "/bar"}] )"_json
|
|
) == R"( {"bar": [1, 2, 3]} )"_json);
|
|
}
|
|
|
|
SECTION("copy")
|
|
{
|
|
CHECK(R"( {"foo": [1, 2, 3]} )"_json.patch(
|
|
R"( [{"op": "copy", "from": "/foo/1", "path": "/bar"}] )"_json
|
|
) == R"( {"foo": [1, 2, 3], "bar": 2} )"_json);
|
|
}
|
|
|
|
SECTION("copy")
|
|
{
|
|
CHECK_NOTHROW(R"( {"foo": "bar"} )"_json.patch(
|
|
R"( [{"op": "test", "path": "/foo", "value": "bar"}] )"_json));
|
|
}
|
|
}
|
|
|
|
SECTION("Tests from github.com/json-patch/json-patch-tests")
|
|
{
|
|
for (const auto* filename :
|
|
{
|
|
TEST_DATA_DIRECTORY "/json-patch-tests/spec_tests.json",
|
|
TEST_DATA_DIRECTORY "/json-patch-tests/tests.json"
|
|
})
|
|
{
|
|
CAPTURE(filename)
|
|
std::ifstream f(filename);
|
|
json const suite = json::parse(f);
|
|
|
|
for (const auto& test : suite)
|
|
{
|
|
INFO_WITH_TEMP(test.value("comment", ""));
|
|
|
|
// skip tests marked as disabled
|
|
if (test.value("disabled", false))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const auto& doc = test["doc"];
|
|
const auto& patch = test["patch"];
|
|
|
|
if (test.count("error") == 0) // NOLINT(readability-container-contains)
|
|
{
|
|
// if an expected value is given, use it; use doc otherwise
|
|
const auto& expected = test.value("expected", doc);
|
|
CHECK(doc.patch(patch) == expected);
|
|
}
|
|
else
|
|
{
|
|
CHECK_THROWS(doc.patch(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&);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("JSON patch - remove with primitive or null parent (regression #5396)")
|
|
{
|
|
// Regression test for https://github.com/nlohmann/json/issues/5396
|
|
//
|
|
// RFC 6902 (§4.2) requires the target location of a "remove" operation
|
|
// to exist. When the target's parent resolves to a primitive value or
|
|
// null, the operation must fail. Previously operation_remove silently
|
|
// did nothing in this case (neither the "is_object" nor the "is_array"
|
|
// branch matched, and there was no final "else"), so the patch appeared
|
|
// to succeed without changing the document. It now throws
|
|
// out_of_range.413.
|
|
|
|
SECTION("parent is a primitive (number)")
|
|
{
|
|
json const doc = {{"a", 1}};
|
|
json const patch = {{{"op", "remove"}, {"path", "/a/b"}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.413] (/a) cannot remove value: the JSON Patch 'remove' 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.413] cannot remove value: the JSON Patch 'remove' target's parent is of type number, but must be an object or array", json::out_of_range&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("parent is a primitive (string)")
|
|
{
|
|
json const doc = {{"foo", {{"bar", "a string"}}}};
|
|
json const patch = {{{"op", "remove"}, {"path", "/foo/bar/baz"}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.413] (/foo/bar) cannot remove value: the JSON Patch 'remove' 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.413] cannot remove value: the JSON Patch 'remove' target's parent is of type string, but must be an object or array", json::out_of_range&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("top-level document is null")
|
|
{
|
|
json const doc = nullptr;
|
|
json const patch = {{{"op", "remove"}, {"path", "/a"}}};
|
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.413] cannot remove value: the JSON Patch 'remove' target's parent is of type null, but must be an object or array", json::out_of_range&);
|
|
}
|
|
|
|
SECTION("legitimate removes still work")
|
|
{
|
|
// object member
|
|
json const doc1 = {{"a", 1}, {"b", 2}};
|
|
json const patch1 = {{{"op", "remove"}, {"path", "/a"}}};
|
|
CHECK(doc1.patch(patch1) == json({{"b", 2}}));
|
|
|
|
// array element
|
|
json const doc2 = R"([1, 2, 3])"_json;
|
|
json const patch2 = {{{"op", "remove"}, {"path", "/1"}}};
|
|
CHECK(doc2.patch(patch2) == R"([1, 3])"_json);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("JSON patch - move where 'from' is a proper prefix of 'path' (regression #5397)")
|
|
{
|
|
// Regression test for https://github.com/nlohmann/json/issues/5397
|
|
//
|
|
// RFC 6902 (§4.4) forbids "from" from being a proper prefix of "path"
|
|
// for a "move" operation: "a location cannot be moved into one of its
|
|
// children." "move" is implemented as remove-then-add; for an object
|
|
// target this happened to throw anyway as a side effect of the "add"
|
|
// step re-resolving through the now-removed parent, but for an array
|
|
// target the removal shifted subsequent indices, so "path" silently
|
|
// re-resolved to a different element and the operation "succeeded"
|
|
// with a corrupted result. It now throws out_of_range.414 for both
|
|
// object and array targets.
|
|
|
|
SECTION("array target (from the issue)")
|
|
{
|
|
json const doc = R"([[1,2],[3]])"_json;
|
|
json const patch = {{{"op", "move"}, {"from", "/0"}, {"path", "/0/0"}}};
|
|
#if JSON_DIAGNOSTIC_POSITIONS
|
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.414] (bytes 0-11) cannot move value: 'from' path '/0' is a proper prefix of 'path' '/0/0'", json::out_of_range&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.414] cannot move value: 'from' path '/0' is a proper prefix of 'path' '/0/0'", json::out_of_range&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("object target")
|
|
{
|
|
json const doc = R"({"a": {"b": 1}})"_json;
|
|
json const patch = {{{"op", "move"}, {"from", "/a"}, {"path", "/a/b"}}};
|
|
#if JSON_DIAGNOSTIC_POSITIONS
|
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.414] (bytes 0-15) cannot move value: 'from' path '/a' is a proper prefix of 'path' '/a/b'", json::out_of_range&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.414] cannot move value: 'from' path '/a' is a proper prefix of 'path' '/a/b'", json::out_of_range&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("from == path is not a proper prefix and must not be rejected")
|
|
{
|
|
// "from" equal to "path" is a no-op move; it is not a *proper*
|
|
// prefix relationship, so this new check must not reject it.
|
|
json const doc = R"({"a": 1, "b": 2})"_json;
|
|
json const patch = {{{"op", "move"}, {"from", "/a"}, {"path", "/a"}}};
|
|
CHECK(doc.patch(patch) == doc);
|
|
}
|
|
|
|
SECTION("raw string prefix that is not a pointer-token prefix must be allowed")
|
|
{
|
|
// "/ab" is a string-prefix of "/abc/x" as raw text, but "ab" and
|
|
// "abc" are different reference tokens, so this is NOT a
|
|
// pointer-token prefix relationship and the move must succeed.
|
|
// This is the key case proving the check compares tokens, not
|
|
// raw pointer text (a naive std::string prefix/rfind check on
|
|
// the undecoded pointer would wrongly reject this).
|
|
json const doc = R"({"ab": 1, "abc": {"x": 2}})"_json;
|
|
json const patch = {{{"op", "move"}, {"from", "/ab"}, {"path", "/abc/x"}}};
|
|
json const result = R"({"abc": {"x": 1}})"_json;
|
|
CHECK(doc.patch(patch) == result);
|
|
}
|
|
|
|
SECTION("escaped reference tokens are compared unescaped")
|
|
{
|
|
// "from" is the single token "a/b" (escaped as "a~1b"); "path"
|
|
// addresses member "x" of that same value, so "from" is a
|
|
// proper (token-level) prefix of "path" and must be rejected.
|
|
json const doc = R"({"a/b": {"x": 1}})"_json;
|
|
json const patch = {{{"op", "move"}, {"from", "/a~1b"}, {"path", "/a~1b/x"}}};
|
|
#if JSON_DIAGNOSTIC_POSITIONS
|
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.414] (bytes 0-17) cannot move value: 'from' path '/a~1b' is a proper prefix of 'path' '/a~1b/x'", json::out_of_range&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.414] cannot move value: 'from' path '/a~1b' is a proper prefix of 'path' '/a~1b/x'", json::out_of_range&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("ordinary valid moves still work")
|
|
{
|
|
// unrelated top-level members
|
|
json const doc1 = R"({"a": 1, "b": 2})"_json;
|
|
json const patch1 = {{{"op", "move"}, {"from", "/a"}, {"path", "/c"}}};
|
|
CHECK(doc1.patch(patch1) == R"({"b": 2, "c": 1})"_json);
|
|
|
|
// sibling paths that share a textual prefix but are unrelated
|
|
json const doc2 = R"({"a": {"x": 1}, "b": {"y": 2}})"_json;
|
|
json const patch2 = {{{"op", "move"}, {"from", "/a/x"}, {"path", "/b/z"}}};
|
|
CHECK(doc2.patch(patch2) == R"({"a": {}, "b": {"y": 2, "z": 1}})"_json);
|
|
|
|
// "path" is a proper prefix of "from" (the reverse relationship,
|
|
// which RFC 6902 does not forbid)
|
|
json const doc3 = R"({"a": {"b": 1}})"_json;
|
|
json const patch3 = {{{"op", "move"}, {"from", "/a/b"}, {"path", "/a"}}};
|
|
CHECK(doc3.patch(patch3) == R"({"a": 1})"_json);
|
|
}
|
|
|
|
SECTION("root 'from' is a proper prefix of every non-root 'path'")
|
|
{
|
|
// the whole document is a proper prefix of any location inside it
|
|
json const doc = R"({"a": 1})"_json;
|
|
json const patch = {{{"op", "move"}, {"from", ""}, {"path", "/a"}}};
|
|
#if JSON_DIAGNOSTIC_POSITIONS
|
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.414] (bytes 0-8) cannot move value: 'from' path '' is a proper prefix of 'path' '/a'", json::out_of_range&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.414] cannot move value: 'from' path '' is a proper prefix of 'path' '/a'", json::out_of_range&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("root 'path' is never a proper prefix violation for a non-root 'from'")
|
|
{
|
|
// the reverse of the above: moving a non-root location to the root
|
|
// is the "path is a prefix of from" relationship, which RFC 6902
|
|
// permits (already covered generally above; this pins the root
|
|
// case specifically, since root is the one path with no reference
|
|
// tokens at all)
|
|
json const doc = R"({"a": {"b": 1}})"_json;
|
|
json const patch = {{{"op", "move"}, {"from", "/a"}, {"path", ""}}};
|
|
CHECK(doc.patch(patch) == R"({"b": 1})"_json);
|
|
}
|
|
|
|
SECTION("the array-append token '-' is an ordinary child token")
|
|
{
|
|
// "-" (append-to-array) addresses a location *inside* the array,
|
|
// so "from" pointing at the array is still a proper prefix of
|
|
// "path" ending in "-" and must be rejected like any other child.
|
|
json const doc = R"({"a": [1, 2]})"_json;
|
|
json const patch = {{{"op", "move"}, {"from", "/a"}, {"path", "/a/-"}}};
|
|
#if JSON_DIAGNOSTIC_POSITIONS
|
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.414] (bytes 0-13) cannot move value: 'from' path '/a' is a proper prefix of 'path' '/a/-'", json::out_of_range&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.414] cannot move value: 'from' path '/a' is a proper prefix of 'path' '/a/-'", json::out_of_range&);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
TEST_CASE("JSON patch - diff emits array removals in descending index order")
|
|
{
|
|
SECTION("array shrunk to empty")
|
|
{
|
|
json const source = {0, 1, 2, 3, 4};
|
|
json const target = json::array();
|
|
|
|
json const patch = json::diff(source, target);
|
|
|
|
json const expected = R"(
|
|
[
|
|
{"op": "remove", "path": "/4"},
|
|
{"op": "remove", "path": "/3"},
|
|
{"op": "remove", "path": "/2"},
|
|
{"op": "remove", "path": "/1"},
|
|
{"op": "remove", "path": "/0"}
|
|
]
|
|
)"_json;
|
|
|
|
CHECK(patch == expected);
|
|
CHECK(source.patch(patch) == target);
|
|
}
|
|
|
|
SECTION("array partially shrunk, after a replacement at a common index")
|
|
{
|
|
json const source = {0, 1, 2, 3, 4};
|
|
json const target = {0, 9};
|
|
|
|
json const patch = json::diff(source, target);
|
|
|
|
// the replacement comes first, then the removals, highest index first
|
|
json const expected = R"(
|
|
[
|
|
{"op": "replace", "path": "/1", "value": 9},
|
|
{"op": "remove", "path": "/4"},
|
|
{"op": "remove", "path": "/3"},
|
|
{"op": "remove", "path": "/2"}
|
|
]
|
|
)"_json;
|
|
|
|
CHECK(patch == expected);
|
|
CHECK(source.patch(patch) == target);
|
|
}
|
|
|
|
SECTION("nested array shrunk")
|
|
{
|
|
json const source = {{"a", {0, 1, 2}}};
|
|
json const target = {{"a", json::array()}};
|
|
|
|
json const patch = json::diff(source, target);
|
|
|
|
json const expected = R"(
|
|
[
|
|
{"op": "remove", "path": "/a/2"},
|
|
{"op": "remove", "path": "/a/1"},
|
|
{"op": "remove", "path": "/a/0"}
|
|
]
|
|
)"_json;
|
|
|
|
CHECK(patch == expected);
|
|
CHECK(source.patch(patch) == target);
|
|
}
|
|
|
|
SECTION("many removals still round-trip")
|
|
{
|
|
json source = json::array();
|
|
for (int i = 0; i < 1000; ++i)
|
|
{
|
|
source.push_back(i);
|
|
}
|
|
json const target = json::array();
|
|
|
|
json const patch = json::diff(source, target);
|
|
|
|
CHECK(patch.size() == 1000);
|
|
CHECK(patch.front().at("path") == "/999");
|
|
CHECK(patch.back().at("path") == "/0");
|
|
CHECK(source.patch(patch) == target);
|
|
}
|
|
}
|