Class JsonPatch<P>
- Type Parameters:
P- the node type of the model of the patch document
A JSON Patch (RFC 6902): a sequence of operations that changes a JSON document.
parse(Object, JsonModel) reads a patch document with the model of any JSON library. The
patch keeps the values of its operations as nodes of that document. apply(N, M) then changes a
target document in place, through a model that is also a JsonFactory and a JsonEditor, for
example the Jackson model:
// Keep numbers exact, and reject duplicate names, so that a patch with two "op" members fails.
JsonMapper mapper = JsonMapper.builder()
.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
.enable(StreamReadFeature.STRICT_DUPLICATE_DETECTION)
.build();
JsonNode document = mapper.readTree("""
{"baz": "qux", "foo": "bar"}""");
JsonNode patchDocument = mapper.readTree("""
[{"op": "replace", "path": "/baz", "value": "boo"},
{"op": "add", "path": "/hello", "value": ["world"]},
{"op": "remove", "path": "/foo"}]""");
// Read the patch with any model, and apply it in place with an editable model.
Result<JsonNode, PatchError> result = JsonPatch.parse(patchDocument, JacksonJsonModel.INSTANCE)
.flatMap(patch -> patch.apply(document, JacksonJsonModel.INSTANCE));
switch (result) {
case Result.Ok<JsonNode, PatchError>(JsonNode root) -> lines.add("patched: " + root);
case Result.Err<JsonNode, PatchError>(PatchError error) -> lines.add(error.message());
}
// patched: {"baz":"boo","hello":["world"]}
// A patch is atomic: after an error, the document has its original value.
JsonNode failing = mapper.readTree("""
[{"op": "replace", "path": "/baz", "value": 42},
{"op": "test", "path": "/baz", "value": "C"}]""");
Result<JsonNode, PatchError> failed = JsonPatch.parse(failing, JacksonJsonModel.INSTANCE)
.flatMap(patch -> patch.apply(document, JacksonJsonModel.INSTANCE));
// failed: Err[error=TestFailed[operation=1]], document: {"baz":"boo","hello":["world"]}
applyToCopy(N, M) builds a changed copy and does not change the document. It needs only a
JsonModel and a JsonFactory, so it also works for immutable values, for example
kotlinx.serialization trees.
| Method | Target model | The document | copy |
|---|---|---|---|
apply(N, M) |
JsonModel, JsonFactory, JsonEditor |
changes in place | one deep copy |
applyToCopy(N, M) |
JsonModel, JsonFactory |
does not change | shares its source |
Atomic application
The operations apply in order. If an operation fails, the method returns the PatchError
(RFC 6902, Section 5), and the document has its original value. apply(N, M) reverses the changes
that it made. Because the order of the members of an object is not significant (RFC 8259,
Section 4), a reversed member can be at a different position in its object. applyToCopy(N, M)
never changes the document.
Copies
The patch reads its document and the target document directly. move moves a node without a
copy, and test compares without a copy. add and replace copy their value once into the
target, so that the target never shares a node with the patch. apply(N, M) makes one deep copy
for copy, so that two locations do not share a node. applyToCopy(N, M) builds a new container
for each container on a changed path, and shares all other nodes.
Duplicate member names
An operation must have exactly one op and one path member (RFC 6902, Section 4). The patch
finds duplicates through JsonModel.hasDuplicate(Object, ca.marcusdunn.jsonlens.model.JsonString),
so read the patch document with a model that keeps duplicate names (the memory-mapped model),
or with a parser that rejects them (Jackson with StreamReadFeature.STRICT_DUPLICATE_DETECTION).
A parser that keeps only one of the values hides the duplicate from the patch.
-
Method Summary
Modifier and TypeMethodDescription<N, M extends JsonModel<N> & JsonFactory<N> & JsonEditor<N>>
Result<N, PatchError> apply(N document, M target) Applies the patch to a document, in place.<N, M extends JsonModel<N> & JsonFactory<N>>
Result<N, PatchError> applyToCopy(N document, M target) Applies the patch to a copy of a document.model()Returns the model of the values of the operations.static <P> JsonPatch<P> Returns a patch with the given operations.Returns the operations.static <P> Result<JsonPatch<P>, PatchError> Reads a patch document (RFC 6902, Sections 3 and 4).
-
Method Details
-
parse
Reads a patch document (RFC 6902, Sections 3 and 4).
The document must be an array of operation objects. Each operation must have exactly one
opand onepath, and the members that its operation needs. Other members are ignored.- Type Parameters:
P- the node type of the model- Parameters:
document- the patch documentmodel- the model of the patch document- Returns:
- the patch, or the
PatchErrorof the first operation that is not valid
-
of
-
operations
-
model
-
apply
public <N, M extends JsonModel<N> & JsonFactory<N> & JsonEditor<N>> Result<N, PatchError> apply(N document, M target) Applies the patch to a document, in place.
The method changes the containers of the document through the model. An operation with the whole document as its target (
"") replaces the root, so use the returned root after the patch.- Type Parameters:
N- the node type of the target documentM- the type of the target model- Parameters:
document- the root of the target documenttarget- the model of the target document- Returns:
- the root of the changed document, or the
PatchErrorof the first operation that failed. After an error, the document has its original value.
-
applyToCopy
public <N, M extends JsonModel<N> & JsonFactory<N>> Result<N, PatchError> applyToCopy(N document, M target) Applies the patch to a copy of a document. The document does not change.
The method needs only a
JsonModeland aJsonFactory, so it works for immutable values, for example kotlinx.serialization trees. For each change, it builds a new container for the changed parent and for each ancestor (path copying). The result shares all other nodes with the document, so do not change one of them in place while you use the other.copyshares its source node for the same reason. The values ofaddandreplaceare copied once from the patch document.JsonNode document = mapper.readTree(""" {"name": "a", "lines": [{"sku": "x", "qty": 1}], "notes": {"long": "text"}}"""); JsonNode patchDocument = mapper.readTree(""" [{"op": "replace", "path": "/lines/0/qty", "value": 2}]"""); JsonNode changed = JsonPatch.parse(patchDocument, JacksonJsonModel.INSTANCE) .flatMap(patch -> patch.applyToCopy(document, JacksonJsonModel.INSTANCE)) .orElse(document); // document: {"name":"a","lines":[{"sku":"x","qty":1}],"notes":{"long":"text"}} (not changed) // changed: {"name":"a","lines":[{"sku":"x","qty":2}],"notes":{"long":"text"}} // Only the root, "/lines", and "/lines/0" are new. "/notes" is the same node in both.- Type Parameters:
N- the node type of the target documentM- the type of the target model- Parameters:
document- the root of the target document. It does not change.target- the model of the target document- Returns:
- the root of the changed copy, or the
PatchErrorof the first operation that failed
-