If you are working with Cluster API, Kubernetes or Kustomize, you may have already encountered JSON Patch in some form. It is a convenient way of specifying mutating operations on JSON objects using a predefined set of patch operations. RFC 6902 defines these guidelines in detail.
History
JSON patch is based on the idea of XML Patch Operations Framework RFC 5261. RFC 5261 allows incremental changes to be specified for XML documents using XML Path Language (XPath) Selectors and XML. Extending the same idea JSON Patch uses a JSON Pointer and JSON document to specify the change operation.
Anatomy of an Operation
JSON Pointer RFC 6901
To be able to specify a change in a JSON document we need to be able to specify the location in the JSON tree where we want to make the changes. Imagine we wanted to make changes to the first name to capitalize it. If we were using Javascript or any other object-oriented programming language we would have written something equivalent to name.first=”Deepak”
.
Based on ideas of XML Path Language (XPath) Selectors, JSON Pointer also uses /
to specify object hierarchy.
A few noticeable rules in JSON Pointer /
and ~
are reserved characters. If your member name contains /
and ~
worry not you can always use ~1
and ~0
.
Operation Type
JSON Patch allows 6 different types of operations and one among them is a little special as it’s not a mutating operation.
add
replace
remove
move
copy
test
Value
A value can be any valid JSON object, in our case it is a string, but this can be an array or object itself.
JSON Patch
A JSON Patch is an array of operations that are evaluated in the specified order.
Operations
add
Add operation allows the creation or update of the value that is present at a given location.
// Original Object
{
"name": {
"first": "Deepak"
}
}
// Patch
[
{
"op": "add",
"path": "/name/last",
"value": "Sharma"
}
]
// Final Obect
{
"name": {
"first": "Deepak",
"last" : "Sharma"
}
}
Adding to an array
// Original Object
[
{
"name": {
"first": "Deepak",
"last": "Sharma"
}
}
]
// Patch specifying index
[
{
"op": "add",
"path": "/1",
"value": {
"name": {
"first": "गोपाल",
"last": "Engineering"
}
}
}
]
// Final Obect
[
{
"name": {
"first": "Deepak",
"last": "Sharma"
}
},
{
"name": {
"first": "गोपाल",
"last": "Engineering"
}
}
]
One interesting feature is that if a value exists at a location and we provide an object or array JSON Patch does not specify that it will be recursively merged further, but simply put it will be replaced with the new content. So while sending objects and arrays please ensure that the value contains full copy of the desired object.
// Original Object
[
{
"name": {
"first": "Deepak",
"last": "Sharma"
},
"interests": [
"coding",
"writing"
],
"education": {
"schools": [
"Alphonsa High School",
"Jawahar Navodaya Vidyalaya"
]
}
}
]
// Patch specifying already existing objects
// will completely replace the exsiting objects
[
{
"op": "add",
"path": "/0/interests",
"value": [
"reading"
]
},
{
"op": "add",
"path": "/0/education",
"value": {
"collage": "NIT Bhopal"
}
}
]
// Final Obect
[
{
"education": {
"collage": "NIT Bhopal"
},
"interests": [
"reading"
],
"name": {
"first": "Deepak",
"last": "Sharma"
}
}
]
See how instead of deep merging nested objects /0/education
and /0/interests
the values were completely replaced.
I am emphasizing this because this particular behaviour of JSON Patch has led to bugs and discussions around CAPI.
remove
Remove operation is fairly simple you just need to provide a path no need to specify value.
// Original Object
[
{
"name": {
"first": "Deepak",
"last": "Sharma",
"middle": ""
}
}
]
// As I don't use middle name
// it's perfectly fine to remove the property
[
{
"op": "remove",
"path": "/0/name/middle"
}
]
// Final Obect
[
{
"name": {
"first": "Deepak",
"last": "Sharma"
}
}
]
replace
Replace operation allows to specify a replace value operation for a given member.
Let’s correct the case in my last name and change it from sharma
to Sharma
// Original Object
[
{
"name": {
"first": "Deepak",
"last": "sharma",
}
}
]
// replace must contain value
[
{
"op": "replace",
"path": "/0/name/last",
"value": "Sharma"
}
]
// Final Obect
[
{
"name": {
"first": "Deepak",
"last": "Sharma"
}
}
]
A replace operation can be written as equivalent to
// replace can be written as subsequent remove and add operation
[
{
"op": "replace",
"path": "/0/name/last",
"value": "Sharma"
}
]
// equiavalent
[
{
"op": "remove",
"path": "/0/name/last"
},
{
"op": "add",
"path": "/0/name/last",
"value": "Sharma"
}
]
move
Move operations is a tree mutation operation where you specify a source JSON Pointer in the JSON tree and a destination location and everything under the source location is moved to the destination JSON Pointer.
// Source Object
[
{
"name": {
"first": "Deepak",
"last": "Sharma"
},
"interests": ["kuberentes"]
},
{
"name": {
"first": "गोपाल",
"last": "Engineering"
}
}
]
// move patch to move my interests to
// गोपाल keywords
[
{
"op": "move",
"from": "/0/interests",
"path": "/1/keywords"
}
]
// resulting object
[
{
"name": {
"first": "Deepak",
"last": "Sharma"
}
},
{
"name": {
"first": "गोपाल",
"last": "Engineering"
},
"keywords": [
"kubernetes"
]
}
]
A move operation is logically equivalent to performing remove at source JSON Pointer followed by adding at destination JSON Pointer
// move patch to move my interests to
// गोपाल keywords
[
{
"op": "move",
"from": "/0/interests",
"path": "/1/keywords"
}
]
// can be rewritten as
[
{
"op": "remove",
"path": "/0/interests"
},
{
"op": "add",
"path": "/1/keywords",
"value": ["kubernetes"]
}
]
copy
Copy operation allows to specify tree mutation where values from the source JSON Pointer are copied to the target JSON Pointer.
// Source Object
[
{
"name": {
"first": "Deepak",
"last": "Sharma"
},
"interests": ["kuberentes"]
},
{
"name": {
"first": "गोपाल",
"last": "Engineering"
}
}
]
// copy to add my interests to
// गोपाल keywords
[
{
"op": "copy",
"from": "/0/interests",
"path": "/1/keywords"
}
]
// resulting object
[
{
"name": {
"first": "Deepak",
"last": "Sharma"
},
"interests": [
"kubernetes"
]
},
{
"name": {
"first": "गोपाल",
"last": "Engineering"
},
"keywords": [
"kubernetes"
]
}
]
A copy operation is logically equivalent to performing add at target JSON Pointer using values present at source JSON Pointer
// move patch to move my interests to
// गोपाल keywords
[
{
"op": "move",
"from": "/0/interests",
"path": "/1/keywords"
}
]
// can be rewritten as
[
{
"op": "add",
"path": "/1/keywords",
"value": ["kubernetes"]
}
]
test
Test operation is not a mutating operation but a mere equality check for values present at a given JSON Pointer.
// Source Object
[
{
"name": {
"first": "Deepak",
"last": "Sharma"
},
"interests": ["kuberentes"]
},
{
"name": {
"first": "गोपाल",
"last": "Engineering"
}
}
]
// test if I contain
[
{
"op": "test",
"path": "/0/interests",
"value": ["kubernetes"]
}
]
// result will evaluate to true
In the context of Cluster API ClusterClass variables only add
, remove
and replace
operations are permitted.