Class JsonPatch<P>

java.lang.Object
ca.marcusdunn.jsonlens.patch.JsonPatch<P>
Type Parameters:
P - the node type of the model of the patch document

public final class JsonPatch<P> extends Object

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 Details

    • parse

      public static <P> Result<JsonPatch<P>, PatchError> parse(P document, JsonModel<P> model)

      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 op and one path, and the members that its operation needs. Other members are ignored.

      Type Parameters:
      P - the node type of the model
      Parameters:
      document - the patch document
      model - the model of the patch document
      Returns:
      the patch, or the PatchError of the first operation that is not valid
    • of

      public static <P> JsonPatch<P> of(List<Operation<P>> operations, JsonModel<P> model)
      Returns a patch with the given operations.
      Type Parameters:
      P - the node type of the model
      Parameters:
      operations - the operations, in order
      model - the model of the values of the operations
      Returns:
      the patch
    • operations

      public List<Operation<P>> operations()
      Returns the operations.
      Returns:
      the operations, in order. The list cannot be changed.
    • model

      public JsonModel<P> model()
      Returns the model of the values of the operations.
      Returns:
      the model of the patch document
    • 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 document
      M - the type of the target model
      Parameters:
      document - the root of the target document
      target - the model of the target document
      Returns:
      the root of the changed document, or the PatchError of 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 JsonModel and a JsonFactory, 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. copy shares its source node for the same reason. The values of add and replace are 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 document
      M - 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 PatchError of the first operation that failed