juneau-bean-jsonpatch
The juneau-bean-jsonpatch module provides Java beans for JSON Patch (RFC 6902), the canonical patch document format used by application/json-patch+json. A JSON Patch document is a JSON array of operation objects describing changes (add, remove, replace, move, copy, test) to apply to a target JSON document.
Overview
This module models the JSON Patch wire format as typed @Marshalled POJOs with polymorphic dispatch on the spec's op discriminator. The beans round-trip through JsonSerializer / JsonParser once the parser is told to read the discriminator from the op member.
Key Features
- Abstract
JsonPatchOperationbase — Carries the sharedpathfield (JSON Pointer per RFC 6901) and the@Marshalled(typePropertyName="op", dictionary={...})discriminator wiring. - Six concrete subclasses —
AddOp,RemoveOp,ReplaceOp,MoveOp,CopyOp,TestOp— each annotated with the matching@Marshalled(typeName=...)value. - Top-level array via
extends LinkedList—JsonPatchextendsLinkedList<JsonPatchOperation>so the document serialises as a top-level JSON array per the RFC (no@Json(wrapperAttr=...)wrapping). Mirrors the pattern used byJsonSchemaArray. - Open for extension —
JsonPatchOperationisabstract, notsealed, so downstream callers can extend the dictionary with custom ops (vendor-specific test operators, JSON Patch extensions, etc.). - No external dependencies — Only depends on
juneau-marshall.
Bean Classes
| Class | Wire op | Extra members |
|---|---|---|
JsonPatchOperation | — (abstract) | path (JSON Pointer) |
AddOp | "add" | value |
RemoveOp | "remove" | — |
ReplaceOp | "replace" | value |
MoveOp | "move" | from (JSON Pointer) |
CopyOp | "copy" | from (JSON Pointer) |
TestOp | "test" | value |
JsonPatch | — | LinkedList<JsonPatchOperation> — the document itself |
Serializer / parser configuration
The dictionary wiring tells the parser how to map op ⇒ subclass, but the serializer still needs to know to emit the op discriminator. JsonPatchMarshaller ships a pre-wired DEFAULT serializer/parser pair with that discriminator already configured, so callers don't need to hand-wire typePropertyName(JsonPatchOperation.class,"op") themselves:
import org.apache.juneau.bean.jsonpatch.*;
// Pre-configured serializer/parser, wired for the "op" discriminator.
JsonSerializer ser = JsonPatchMarshaller.SERIALIZER;
JsonParser par = JsonPatchMarshaller.PARSER;
If you need custom settings, build your own serializer/parser and still apply the same typePropertyName wiring:
import org.apache.juneau.marshall.json.*;
import org.apache.juneau.bean.jsonpatch.*;
JsonSerializer ser = JsonSerializer.create()
.addBeanTypes()
.typePropertyName(JsonPatchOperation.class, "op")
.build();
JsonParser par = JsonParser.create()
.typePropertyName(JsonPatchOperation.class, "op")
.build();
Basic Usage
Building a patch document
import org.apache.juneau.bean.jsonpatch.*;
JsonPatch patch = new JsonPatch()
.append(new TestOp("/a/b/c", "foo"))
.append(new RemoveOp("/a/b/c"))
.append(new AddOp("/a/b/c", java.util.List.of("foo", "bar")))
.append(new ReplaceOp("/a/b/c", 42))
.append(new MoveOp("/a/b/d", "/a/b/c"))
.append(new CopyOp("/a/b/e", "/a/b/d"));
// Static shortcuts on JsonPatchMarshaller use the pre-wired DEFAULT serializer/parser.
String wire = JsonPatchMarshaller.of(patch);
JsonPatch back = JsonPatchMarshaller.to(wire, JsonPatch.class);
The wire form is a top-level JSON array of operation objects, matching the example in RFC 6902 §3:
[
{"op":"test","path":"/a/b/c","value":"foo"},
{"op":"remove","path":"/a/b/c"},
{"op":"add","path":"/a/b/c","value":["foo","bar"]},
{"op":"replace","path":"/a/b/c","value":42},
{"op":"move","from":"/a/b/c","path":"/a/b/d"},
{"op":"copy","from":"/a/b/d","path":"/a/b/e"}
]
Polymorphic dispatch
The parser materialises the concrete subclass keyed off the wire op value. Each entry in the parsed JsonPatch is an instance of the matching subclass — no manual instanceof / switch needed:
JsonPatch back = JsonPatchMarshaller.to(wire, JsonPatch.class);
for (JsonPatchOperation op : back) {
if (op instanceof AddOp add) { /* ... */ }
else if (op instanceof RemoveOp rem) { /* ... */ }
else if (op instanceof ReplaceOp rp) { /* ... */ }
// ...
}
Extending with a custom operation
JsonPatchOperation is abstract, not sealed, so callers can add their own ops on top of the standard six. Subclass JsonPatchOperation, annotate with @Marshalled(typeName="your-op-name"), and register the subclass in a custom dictionary on your serializer / parser builder.
Content-Type
The matching Content-Type constant lives in juneau-rest-common and has been available since 9.2.x:
import org.apache.juneau.http.header.ContentType;
ContentType.APPLICATION_JSON_PATCH; // "application/json-patch+json"