JSON Patch specification (RFC 6902)
I am working on something that needs to use a JSON Patch and the examples in the RFC 6902 are a bit cryptic. I found better examples, but they were a bit too much boiled down, leaving out important quirks about the RFC. So here is a combination of both:
There are six basic operations, that operate on JSON:
I’m only writing about the first four operations here, because I didn’t need to use the last two (copy and test) in my project. They are described in the RFC 6902 though.
JSON Format
JSON follows a specific format according to RFC 8259. It boils down to these data formats:
- Value:
- Literal: (either string, number, boolean or
null
) - Object
- Array
- Literal: (either string, number, boolean or
- Object: Unordered, possibly empty, collection of key-value pairs enclosed by
{}
- Key-Value pair:
- Key: called “names”, are string literals, unique within the object
- Value
- Array: Ordered, possibly empty, start with index 0, list of values enclosed by
[]
Example JSON
This is the example I’m working on for every operation:
{
"tea": [
{ "name": "Green tea" },
{ "name": "Earl Grey" }
],
"milk" : false
}
The JSON Patch operations
A patch is made up of objects that are contained in an array.
They follow a specific syntax, but each contains a key named "op"
with a string value that defines the operation.
"path"
is also in every object, but it’s used in the same context every time.
There are also the keys "value"
and "from"
, but they are not always used.
Add
Adding an object element
Example:
[
{ "op": "add", "path": "/coffee", "value": "Espresso" }
]
Operation key-value pair:
"op": "add"
Path, where to add things:
"path": "/coffee"
In this case, we define the key "coffee"
within the root object
Value, what should be added:
"value": "Espresso"
This will add the value "Espresso"
to the key "coffee"
.
Result:
{
"tea": [
{ "name": "Green tea" },
{ "name": "Earl Grey" }
],
"milk" : false,
"coffee" : "Espresso"
}
Adding more complicated objects
If we were to add a very big cascaded object with tons of sub-objects, we would still just use the same "path"
but the whole object with sub-objects in the value.
For an object and sub-object, which contains an array:
[
{ "op": "add", "path": "/coffee", "value": { "italian" : { "hot": [ "Espresso" ] } } }
]
=>
{
"tea": [
{ "name": "Green tea" },
{ "name": "Earl Grey" }
],
"milk" : false,
"coffee" : {
"italian" : {
"hot": [ "Espresso" ] }
}
}
Adding an array element
It’s not that different if we add to arrays, we just need to consider an index.
Example:
[
{ "op": "add", "path": "/tea/1", "value": { "name": "Fruit tea" } }
]
Operation key-value pair:
"op": "add"
Path, where to add things:
"path": "/tea/1"
It’s added at the given index, pushing all indices at or higher than the given one, one space up. (i.e. add object at 1, (old) 1 -> 2, (old) 2 -> 3, etc.) The index must not be greater than size of the array.
In this array, that is mapped to the key "tea"
, the value will be added at index 1
.
The object that is currently there is { "name": "Earl Grey" }
.
Using the index -
instead of a number, will append the object to the end of the array.
Value, what should be added:
"value": { "name": "Fruit tea" }
Which will just add the key-value pair with the key "name"
and the value "Fruit tea"
Result:
{
"tea": [
{ "name": "Green tea" },
{ "name": "Fruit tea" },
{ "name": "Earl Grey" }
],
"milk" : false
}
Weird adding behavior
If there already is an object at the specified "path"
, that object is replaced by the specified "value"
.
This is the same behavior a replace operation would have. It’s mentioned in the RFC, but not very precise.
Remove
Remove an object
Example:
[
{ "op": "remove", "path": "/tea" }
]
Operation key-value pair:
"op": "remove"
Path to the object that should be deleted, defined by the path of the object and the key:
"path": "/tea"
We delete the object with the key "tea"
which is located in the root. Any objects within it are deleted as well.
There is no "value"
key-value pair, because the path suffices. The object the path leads to has to exist.
Result:
{
"milk" : false
}
Remove an object within an array
Example:
[
{ "op": "remove", "path": "/tea/0" }
]
Operation key-value pair:
"op": "remove"
Path to the object that should be deleted, defined by the key of the object and its path:
"path": "/tea/0"
Same procedere as before, but now we additionally add an index.
Result:
{
"tea": [
{ "name": "Earl Grey" }
],
"milk" : false
}
Replace
Replacing a literal
Example:
[
{ "op": "replace", "path": "/tea/0/name", "value": "Herbal tea" }
]
Operation key-value pair:
"op": "replace"
Path, where something should be replaced:
"path": "/tea/0/name"
The given path must point to a key, as only a value can be changed by a replace operation, not the name of keys themselves.
Move can change the name of keys directly.
You can, of course, change the key through replacing the whole object, using "/tea/0"
as the path and the object as it’s "value"
.
In this case though, it will follow the path through the value of the key "tea"
, go to index 0
and finally to the value that’s associated to the key "name"
.
The path must exist already.
Value, that replaces the old one:
"value": "Herbal tea"
The new value ("Herbal tea"
) that replaces the old value ("Green tea"
) at the given path.
Result:
{
"tea": [
{ "name": "Herbal tea" },
{ "name": "Earl Grey" }
],
"milk" : false
}
Values in JSON can be objects, literals and arrays, so nothing stops us from putting in any value we like (as long as it is still a value). This means it can be a different type of value.
Example:
[
{ "op": "replace",
"path": "/milk",
"value": [
{ "british" : { "amount": 42, "type" : "whole" } },
{ "normal" : false } ] }
]
=>
{
"tea": [
{ "name": "Herbal tea" },
{ "name": "Earl Grey" }
],
"milk" : [ { "british" : { "amount": 42, "type" : "whole" } },
{ "normal" : false } ]
}
Move
Moving a value
This operation removes a value from one location and adds it to another.
Example:
[
{ "op": "move", "from" : "/milk", "path": "/tea/0/milk" }
]
Operation key-value pair:
"op": "move"
Path to the key, where the value is located before the move operation:
"from": "/milk"
The "from"
location must exist and it can’t be a proper prefix of the "path"
, meaning that a value can’t be moved to one of its children.
Path to the key (in this case, including an index), where it should be located after the operation:
"path": "/tea/0/milk"
The path must include the key as well:
"/tea/0/"
would move the key-value pair "" : false
to the object.
"/tea/0"
would add the value false
directly to the array.
Result:
{
"tea": [
{ "name": "Green tea", "milk" : false },
{ "name": "Earl Grey" }
]
}
Renaming a key
Even though we move some value, it can also be seen as renaming the key.
Example:
[
{ "op": "move", "from" : "/tea", "path": "/leaf-based-beverage" }
]
Operation key-value pair:
"op": "move"
Path to the key, where the value is located before the move operation:
"from": "/tea"
Path to the key, where it should be located after the operation:
"path": "/leaf-based-beverage"
The path up until the last key must exist. You can’t generate a bunch of keys at the same time.
Moving to /leaf/based/beverage
would not work, leaf
and based
need to exist beforehand, beverage
.
(You’d need to add an object and put the keys in sub-objects similar to the one I explained here)
Result:
{
"leaf-based-beverage": [
{ "name": "Green tea" },
{ "name": "Earl Grey" }
],
"milk" : false
}
Replacing another key
We can also just move on another key that already exists, replacing it in doing so:
Example:
[
{ "op": "move", "from" : "/tea", "path": "/milk" }
]
=>
{
"milk": [
{ "name": "Green tea" },
{ "name": "Earl Grey" }
]
}