This project, as of version 1.4, is licensed under both LGPLv3 and ASL 2.0. See file LICENSE for more details. Versions 1.3 and lower are licensed under LGPLv3 only.
Note the "L" in "LGPL". LGPL AND GPL ARE QUITE DIFFERENT!
This library was forked by gravity9 from the original repository to maintain and extend it as the original library is no longer supported.
This is an implementation of RFC 6902 (JSON Patch) and RFC 7386 (JSON Merge Patch) written in Java, which uses Jackson (2.x) at its core.
Its features are:
- {de,}serialization of JSON Patch and JSON Merge Patch instances with Jackson;
- full support for RFC 6902 operations, including
test
; - JSON "diff" (RFC 6902 only) with operation factorization.
- support for
JsonPointer
andJsonPath
The current version is 2.0.2. See file RELEASE-NOTES.md
for details of releases before 1.11.
With Gradle:
dependencies {
compile(group: "com.gravity9", name: "json-patch-path", version: "yourVersionHere");
}
With Maven:
<dependency>
<groupId>com.gravity9</groupId>
<artifactId>json-patch-path</artifactId>
<version>2.0.2</version>
</dependency>
Versions before 1.10 are available at groupId
com.github.fge
and artifactId
json-patch
.
Versions before 1.13 are available at groupId
com.github.java-json-tools
and artifactId
json-patch
.
When computing the difference between two JSON texts (in the form of JsonNode
instances), the diff
will factorize value removals and additions as moves and copies.
For instance, given this node to patch:
{ "a": "b" }
in order to obtain:
{ "c": "b" }
the implementation will return the following patch:
[ { "op": "move", "from": "/a", "path": "/c" } ]
It is able to do even more than that. See the test files in the project.
RFC 6902 mandates that when testing for numeric values, however deeply nested in the tested value, a test is successful if the numeric values are mathematically equal. That is, JSON texts:
1
and:
1.00
must be considered equal.
This implementation obeys the RFC; for this, it uses the numeric equivalence of jackson-coreutils.
You have two choices to build a JsonPatch
instance: use Jackson deserialization, or initialize one
directly from a JsonNode
. Examples:
// Using Jackson
final ObjectMapper mapper = new ObjectMapper();
final InputStream in = ...;
final JsonPatch patch = mapper.readValue(in, JsonPatch.class);
// From a JsonNode
final JsonPatch patch = JsonPatch.fromJson(node);
You can then apply the patch to your data:
// orig is also a JsonNode
final JsonNode patched = patch.apply(orig);
The main class is JsonDiff
. It returns the patch as a JsonPatch
or as a JsonNode
. Sample usage:
final JsonPatch patch = JsonDiff.asJsonPatch(source, target);
final JsonNode patchNode = JsonDiff.asJson(source, target);
It's possible to ignore fields in Json Diff. List of ignored fields should be specified as JsonPointer or JsonPath paths. If ignored field does not exist in target or source object, it's ignored.
final List<String> fieldsToIgnore = new ArrayList<>();
fieldsToIgnore.add("/id");
fieldsToIgnore.add("$.cars[-1:]");
final JsonPatch patch = JsonDiff.asJsonPatch(source, target, fieldsToIgnore);
final JsonNode patchNode = JsonDiff.asJson(source, target, fieldsToIgnore);
Important note: the API offers no guarantee at all about patch "reuse"; that is, the generated patch is only guaranteed to safely transform the given source to the given target. Do not expect it to give the result you expect on another source/target pair!
As for JsonPatch
, you may use either Jackson or "direct" initialization:
// With Jackson
final JsonMergePatch patch = mapper.readValue(in, JsonMergePatch.class);
// With a JsonNode
final JsonMergePatch patch = JsonMergePatch.fromJson(node);
Applying a patch also uses an .apply()
method:
// orig is also a JsonNode
final JsonNode patched = patch.apply(orig);
-
Add field
a
with value1
to object
{ "op": "add", "path": "/a", "value": 1 }
Before:{ "b": "test2" }
After:
{ "a": 1, "b": "test2" }
-
Add element with value
1
at the end of array with namearray
{ "op": "add", "path": "/array/-", "value": 1 }
Before:{ "array": [0, 1, 2, 3] }
After:
{ "array": [0, 1, 2, 3, 1] }
-
Add element with value
1
at the specific index of array with namearray
{ "op": "add", "path": "/array/2", "value": 1 }
Before:{ "array": [0, 1, 2, 3] }
After:
{ "array": [0, 1, 1, 2, 3] }
-
Add element with name
b
into inner object
{ "op": "add", "path": "/obj/inner/b", "value": [ 1, 2 ] }
Before:{ "obj": { "inner": { "a": "test" } } }
After:
{ "obj": { "inner": { "a": "test", "b": [1, 2] } } }
-
If element with name
a
exists, thenadd
operation overrides it.
{ "op": "add", "path": "/a", "value": 1 }
Before:
{ "a": 0 }
After:
{ "a": 1 }
It's possible to add element to JsonNode if it does not exist using JsonPath expressions see more examples of JsonPath
-
Add
color
field tobicycle
object if it doesn't exist
{ "op": "add", "path": "$.store.bicycle[?([email protected])].color", "value": "red" }
Before:
{ "store": { "bicycle": { "price": 19.95 } } }
After:
{ "store": { "bicycle": { "price": 19.95, "color": "red" } } }
-
Add value for
color
field tobicycle
object if it is equal tonull
{ "op": "add", "path": "$.store.bicycle[?(@.color == null)].color", "value": "red" }
Before:
{ "store": { "bicycle": { "price": 19.95, "color": null } } }
After:
{ "store": { "bicycle": { "price": 19.95, "color": "red" } } }
-
Add field
pages
tobook
array ifbook
does not contain this field, or it is equal tonull
{ "op": "add", "path": "$..book[?([email protected] || @.pages == null)].pages", "value": 250 }
Before:
{ "store": { "book": [ { "category": "reference", "author": "Nigel Rees", "title": "Sayings of the Century", "price": 8.95 }, { "category": "fiction", "author": "Herman Melville", "title": "Moby Dick", "isbn": "0-553-21311-3", "price": 8.99, "pages": null }, { "category": "fiction", "author": "J.R.R. Tolkien", "title": "The Lord of the Rings", "isbn": "0-395-19395-8", "price": 22.99, "pages": 100 } ] } }
After:
{ "store": { "book": [ { "category": "reference", "author": "Nigel Rees", "title": "Sayings of the Century", "price": 8.95, "pages": 250 }, { "category": "fiction", "author": "Herman Melville", "title": "Moby Dick", "isbn": "0-553-21311-3", "price": 8.99, "pages": 250 }, { "category": "fiction", "author": "J.R.R. Tolkien", "title": "The Lord of the Rings", "isbn": "0-395-19395-8", "price": 22.99, "pages": 100 } ] } }
-
Remove element with name
a
{ "op": "remove", "path": "/a" }
Before:{ "a": "test", "b": "test2" }
After:
{ "b": "test2" }
-
Remove element from array with name
list
at index2
{ "op": "remove", "path": "/list/2" }
Before:{ "a": "test", "list": [0, 1, 2, 3, 4] }
After:
{ "a": "test", "list": [0, 1, 3, 4] }
-
Replace value for element with name
a
tonew-value
{ "op": "replace", "path": "/a", "value": "new-value"}
Before:
{ "a": "test", "b": "test2" }
After:
{ "a": "new-value", "b": "test2" }
-
Replace value with
new-value
for 2nd element in array with namearray
{ "op": "replace", "path": "/array/2", "value": "new-value"}
Before:{ "a": "test", "array": ["test0", "test1", "test2"] }
After:
{ "a": "test", "array": ["test0", "test1", "new-value"] }
-
Copy value from filed
a
to fieldb
which does not exist
{ "op": "copy", "from": "/a", "path": "/b" }
Before:
{ "a": "test" }
After:
{ "a": "test", "b": "test" }
-
Copy value from filed
a
to fieldb
which exists - value will be updated
{ "op": "copy", "from": "/a", "path": "/b" }
Before:
{ "a": "test", "b": "old value" }
After:
{ "a": "test", "b": "test" }
-
Copy first element of array to the end of array
{ "op": "copy", "from": "/array/0", "path": "/array/-" }
Before:
{ "array": [0, 1, 2], "b": "old value" }
After:
{ "array": [0, 1, 2, 0], "b": "old value" }
-
Move value from field
a
to fieldb
{ "op": "move", "from": "/a", "path": "/b" }
Before:
{ "a": 1, "b": 2 }
After:
{ "b": 1 }
-
Move first element of an array to the end of array
{ "op": "move", "from": "/array/0", "path": "/array/-" }
Before:
{ "array": [1, 2, 3] }
After:
{ "array": [2, 3, 1] }
- Check if field
a
has valuetest-value
{ "op": "test", "path": "/a", "value": "test-value" }
JsonPath is supported in all operations (add
, remove
, copy
, replace
, move
, test
).
Examples of JsonPath:
$.store.bicycle.price
- Get price of bicycle in store$.store.book[*].pages
- Get pages from all books in store$..book[*].pages
- Get pages from all books which are a descent of root node$.store.book[-1:].pages
- Get pages from last book in store$.store.book[:2].pages
- Get pages of first two books in store$.store.book[?(@.author=='J.R.R. Tolkien')].pages
- Get pages of books written by J.R.R. Tolkien$..book[?(@.isbn)].pages
- Get pages of books which containisbn
property$..book[?([email protected])].pages
- Get pages of books which do not containisbn
property$..book[?(@.price < 8.99)].pages
- Get pages of books which price is lower than8.99
$..book[?(@.author =~ /.*Tolkien/i)].pages
- Pages of books whose author name ends with Tolkien (case-insensitive).$..book[?(@.category == 'fiction' || @.category == 'reference')]
- All books infiction
orreference
category$..book[?(@.category in ['fiction', 'reference'])]
- All books infiction
orreference
category$..book[?(@.category nin ['fiction'])]
- All books not infiction
category$..book[?(@.category=='fiction' && @.price < 10)].pages
- List of pages of books infiction
category and price lower than10
$..book[?(@.tags subsetof ['tag1', 'tag2'])]
- All books with list of tags which is subset of['tag1', 'tag2']
$..book[?(@.tags contains 'tag2')]
- All books with list of tags containingtag2
$..book[?(@.tags size 2)]
- All books with list of tags containing exactly 2 elements$..book[?(@.tags empty true)]
- All books with empty list of tags$..book[?(@.tags empty false)]
- All books with not empty list of tags
$..book[(@.length-1)].title
- not supported. Use$..book[-1:].title
instead.