summaryrefslogtreecommitdiff
path: root/docs/handbook/json4cpp/json-patch.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/handbook/json4cpp/json-patch.md')
-rw-r--r--docs/handbook/json4cpp/json-patch.md341
1 files changed, 341 insertions, 0 deletions
diff --git a/docs/handbook/json4cpp/json-patch.md b/docs/handbook/json4cpp/json-patch.md
new file mode 100644
index 0000000000..4c9de8fad5
--- /dev/null
+++ b/docs/handbook/json4cpp/json-patch.md
@@ -0,0 +1,341 @@
+# json4cpp — JSON Patch & Merge Patch
+
+## JSON Patch (RFC 6902)
+
+JSON Patch defines a JSON document structure for expressing a sequence of
+operations to apply to a JSON document.
+
+### `patch()`
+
+```cpp
+basic_json patch(const basic_json& json_patch) const;
+```
+
+Returns a new JSON value with the patch applied. Does not modify the
+original. Throws `parse_error::104` if the patch document is malformed.
+
+```cpp
+json doc = {
+ {"name", "alice"},
+ {"age", 30},
+ {"scores", {90, 85}}
+};
+
+json patch = json::array({
+ {{"op", "replace"}, {"path", "/name"}, {"value", "bob"}},
+ {{"op", "add"}, {"path", "/scores/-"}, {"value", 95}},
+ {{"op", "remove"}, {"path", "/age"}}
+});
+
+json result = doc.patch(patch);
+// {"name": "bob", "scores": [90, 85, 95]}
+```
+
+### `patch_inplace()`
+
+```cpp
+void patch_inplace(const basic_json& json_patch);
+```
+
+Applies the patch directly to the JSON value (modifying in place):
+
+```cpp
+json doc = {{"key", "old"}};
+doc.patch_inplace(json::array({
+ {{"op", "replace"}, {"path", "/key"}, {"value", "new"}}
+}));
+// doc is now {"key": "new"}
+```
+
+### Patch Operations
+
+Each operation is a JSON object with an `"op"` field and operation-specific
+fields:
+
+#### `add`
+
+Adds a value at the target location. If the target exists and is in an
+object, it is replaced. If the target is in an array, the value is inserted
+before the specified index.
+
+```json
+{"op": "add", "path": "/a/b", "value": 42}
+```
+
+The path's parent must exist. The `-` token appends to arrays:
+
+```cpp
+json doc = {{"arr", {1, 2}}};
+json p = json::array({{{"op", "add"}, {"path", "/arr/-"}, {"value", 3}}});
+doc.patch(p); // {"arr": [1, 2, 3]}
+```
+
+#### `remove`
+
+Removes the value at the target location:
+
+```json
+{"op": "remove", "path": "/a/b"}
+```
+
+Throws `out_of_range` if the path does not exist.
+
+#### `replace`
+
+Replaces the value at the target location (equivalent to `remove` + `add`):
+
+```json
+{"op": "replace", "path": "/name", "value": "bob"}
+```
+
+Throws `out_of_range` if the path does not exist.
+
+#### `move`
+
+Moves a value from one location to another:
+
+```json
+{"op": "move", "from": "/a/b", "path": "/c/d"}
+```
+
+Equivalent to `remove` from source + `add` to target. The `from` path
+must not be a prefix of the `path`.
+
+#### `copy`
+
+Copies a value from one location to another:
+
+```json
+{"op": "copy", "from": "/a/b", "path": "/c/d"}
+```
+
+#### `test`
+
+Tests that the value at the target location equals the specified value:
+
+```json
+{"op": "test", "path": "/name", "value": "alice"}
+```
+
+If the test fails, `patch()` throws `other_error::501`:
+
+```cpp
+json doc = {{"name", "alice"}};
+json p = json::array({
+ {{"op", "test"}, {"path", "/name"}, {"value", "bob"}}
+});
+
+try {
+ doc.patch(p);
+} catch (json::other_error& e) {
+ // [json.exception.other_error.501] unsuccessful: ...
+}
+```
+
+### Patch Validation
+
+The `patch()` method validates each operation:
+- `op` must be one of: `add`, `remove`, `replace`, `move`, `copy`, `test`
+- `path` is required for all operations
+- `value` is required for `add`, `replace`, `test`
+- `from` is required for `move`, `copy`
+
+Missing or invalid fields throw `parse_error::105`.
+
+### Operation Order
+
+Operations are applied sequentially. Each operation acts on the result of
+the previous one:
+
+```cpp
+json doc = {};
+json ops = json::array({
+ {{"op", "add"}, {"path", "/a"}, {"value", 1}},
+ {{"op", "add"}, {"path", "/b"}, {"value", 2}},
+ {{"op", "replace"}, {"path", "/a"}, {"value", 10}},
+ {{"op", "remove"}, {"path", "/b"}}
+});
+
+json result = doc.patch(ops);
+// {"a": 10}
+```
+
+## `diff()` — Computing Patches
+
+```cpp
+static basic_json diff(const basic_json& source,
+ const basic_json& target,
+ const string_t& path = "");
+```
+
+Generates a JSON Patch that transforms `source` into `target`:
+
+```cpp
+json source = {{"name", "alice"}, {"age", 30}};
+json target = {{"name", "alice"}, {"age", 31}, {"city", "wonderland"}};
+
+json patch = json::diff(source, target);
+// [
+// {"op": "replace", "path": "/age", "value": 31},
+// {"op": "add", "path": "/city", "value": "wonderland"}
+// ]
+
+// Verify roundtrip
+assert(source.patch(patch) == target);
+```
+
+### Diff Algorithm
+
+The algorithm works recursively:
+1. If `source == target`, produce no operations
+2. If types differ, produce a `replace` operation
+3. If both are objects:
+ - Keys in `source` but not `target` → `remove`
+ - Keys in `target` but not `source` → `add`
+ - Keys in both with different values → recurse
+4. If both are arrays:
+ - Compare element-by-element
+ - Produce `replace` for changed elements
+ - Produce `add` for extra elements in target
+ - Produce `remove` for extra elements in source
+5. For primitives with different values → `replace`
+
+Note: The generated patch uses only `add`, `remove`, and `replace`
+operations (not `move` or `copy`).
+
+### Custom Base Path
+
+The `path` parameter sets a prefix for all generated paths:
+
+```cpp
+json patch = json::diff(a, b, "/config");
+// All paths will start with "/config/..."
+```
+
+## Merge Patch (RFC 7396)
+
+Merge Patch is a simpler alternative to JSON Patch. Instead of an array of
+operations, a merge patch is a JSON object that describes the desired
+changes directly.
+
+### `merge_patch()`
+
+```cpp
+void merge_patch(const basic_json& apply_patch);
+```
+
+Applies a merge patch to the JSON value in place:
+
+```cpp
+json doc = {
+ {"title", "Hello"},
+ {"author", {{"name", "alice"}}},
+ {"tags", {"example"}}
+};
+
+json patch = {
+ {"title", "Goodbye"},
+ {"author", {{"name", "bob"}}},
+ {"tags", nullptr} // null means "remove"
+};
+
+doc.merge_patch(patch);
+// {
+// "title": "Goodbye",
+// "author": {"name": "bob"},
+// }
+// "tags" was removed because the patch value was null
+```
+
+### Merge Patch Rules
+
+The merge patch algorithm (per RFC 7396):
+
+1. If the patch is not an object, replace the target entirely
+2. If the patch is an object:
+ - For each key in the patch:
+ - If the value is `null`, remove the key from the target
+ - Otherwise, recursively merge_patch the target's key with the value
+
+```cpp
+// Partial update — only specified fields change
+json config = {{"debug", false}, {"port", 8080}, {"host", "0.0.0.0"}};
+
+config.merge_patch({{"port", 9090}});
+// {"debug": false, "port": 9090, "host": "0.0.0.0"}
+
+config.merge_patch({{"debug", nullptr}});
+// {"port": 9090, "host": "0.0.0.0"}
+```
+
+### Limitations of Merge Patch
+
+- Cannot set a value to `null` (null means "delete")
+- Cannot manipulate arrays — arrays are replaced entirely
+- Cannot express "move" or "copy" semantics
+
+```cpp
+json doc = {{"items", {1, 2, 3}}};
+doc.merge_patch({{"items", {4, 5}}});
+// {"items": [4, 5]} — array replaced, not merged
+```
+
+## JSON Patch vs. Merge Patch
+
+| Feature | JSON Patch (RFC 6902) | Merge Patch (RFC 7396) |
+|---|---|---|
+| Format | Array of operations | JSON object |
+| Operations | add, remove, replace, move, copy, test | Implicit merge |
+| Array handling | Per-element operations | Replace entire array |
+| Set value to null | Yes (explicit `add`/`replace`) | No (null = delete) |
+| Test assertions | Yes (`test` op) | No |
+| Reversibility | Can `diff()` to reverse | No |
+| Complexity | More verbose | Simpler |
+
+## Complete Example
+
+```cpp
+#include <nlohmann/json.hpp>
+#include <iostream>
+
+using json = nlohmann::json;
+
+int main() {
+ // Original document
+ json doc = {
+ {"name", "Widget"},
+ {"version", "1.0"},
+ {"settings", {
+ {"color", "blue"},
+ {"size", 10},
+ {"enabled", true}
+ }},
+ {"tags", {"production", "stable"}}
+ };
+
+ // JSON Patch: precise operations
+ json patch = json::array({
+ {{"op", "replace"}, {"path", "/version"}, {"value", "2.0"}},
+ {{"op", "add"}, {"path", "/settings/theme"}, {"value", "dark"}},
+ {{"op", "remove"}, {"path", "/settings/size"}},
+ {{"op", "add"}, {"path", "/tags/-"}, {"value", "updated"}},
+ {{"op", "test"}, {"path", "/name"}, {"value", "Widget"}}
+ });
+
+ json patched = doc.patch(patch);
+
+ // Compute diff to verify
+ json computed_patch = json::diff(doc, patched);
+ assert(doc.patch(computed_patch) == patched);
+
+ // Merge Patch: simple update
+ json merge = {
+ {"version", "2.1"},
+ {"settings", {{"color", "red"}}},
+ {"tags", nullptr} // remove tags
+ };
+
+ patched.merge_patch(merge);
+ std::cout << patched.dump(2) << "\n";
+}
+```