Thumbnail: json-patch

JSON Patch specification (RFC 6902)

on under json
9 minute read

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
  • 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" }
  ]
}

My sources for this post

json, json patch, rfc