HTTP PATCH

HTTP PATCH is a companion to the big four RESTful verbs - GET, POST, PUT & DELETE. It can be thought of as a partial PUT which only affects a portion of the resource in question. By limiting the scope of each request and by introducing PATCH operations we can eliminate many of the pitfalls of a traditional PUT endpoint.

The problem with HTTP PUT

PUT endpoints are inherently dangerous for a couple of reasons:

  • Every time an external system performs an update using a PUT it is in a race condition with any other process acting on the same resource. Unless a lot of care is taken the last process to act will often overwrite any changes made by the others.
  • In order to update any part of the resource the user must supply it in its entirety. This forces the external system to know more information about the resource than it cares about and widens the scope for potential race condition clashes.

Introducing HTTP PATCH

HTTP PATCH is described in IETF RFC-5789. The difference between PUT & PATCH is summarised nicely in the quote below:

"In a PUT request, the enclosed entity is considered to be a modified version of the resource stored on the origin server, and the client is requesting that the stored version be replaced. With PATCH, however, the enclosed entity contains a set of instructions describing how a resource currently residing on the origin server should be modified to produce a new version."

Our implementation of JSON HTTP PATCH

The most simple way to implement a PATCH endpoint is to merely allow the user to specify only the fields that they wish to change and to leave all others as they are. Doing it this way is fine but many systems don't handle null values very well in JSON and that makes in difficult to remove a value once it has been set. Also this approach misses one of the requirements of the PATCH specification - that the request be made up of a set of modification instructions.

IETF RFC-6902 describes the JSON syntax for specifying a sequence of patch operations. The example request body looks like this:

[
    { "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" }
]

Each PATCH operation has an 'op' field which specifies which of the six operations is going to be performed and a 'path' field which contains the JSON pointer ( IETF RFC-6901) from the root of the resource to the field being operated on. The different operations and pointer syntax are described in more detail below.

Brightpearl will perform each PATCH operation in sequence and will only update the existing resource if all operations were successful. After it is updated the resource will go through the usual validation and business logic checks before it is saved so this may return error responses. The response from a successful PATCH call is the new state of the resource which will be the same as making a GET request.

To see an example of a PATCH request in action try order custom fields.

PATCH operations

For a full description of each operation refer to IETF RFC-6902. Note that when we say 'value' below you are not limited to JSON primitives, you can pass complex objects or arrays too.

Add

{ "op": "add", "path": "/a/b/c", "value": "foo" }

The operation will:

  • Add a value to an array at the specified index and right-shift the remaining elements OR
  • Add a value with the specified key to a resource OR
  • Overwrite a value at the specified key of a resource
  • It will fail if the parent resource or array specified by the path does not exist.

Remove

{ "op": "remove", "path": "/a/b/c" }

The operation will:

  • Remove the value from the specified key of a resource OR
  • Remove the value from the specified index of an array and left-shift the remaining elements
  • It will fail if the target location does not exist.

It will fail if the target location does not exist.

Replace

{ "op": "replace", "path": "/a/b/c", "value": 42 }

The operation will replace one value with another. A replace operation is effectively a remove and then an add with the same target location. It will fail of any of the conditions for those operations are not met.

Move

{ "op": "move", "from": "/a/b/c", "path": "/a/b/d" }

The operation will remove a value from a resource and add it to another resource. A move operation is effectively a remove and then an add with different target locations. It will fail if any of the conditions for those operations are not met.

Copy

{ "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }

The operation will copy a value from a resource to another. A copy operation is effectively an add with the value taken from an existing resource, it will fail if the source resource does not exist or the add operation fails.

Test

{ "op": "test", "path": "/a/b/c", "value": "foo" }

Asserts that the value at the specified key of a resource is equal to the one supplied. If will fail if the target resource does not exist or the values are not equal.

JSON Pointers

Every PATCH operation requires you to supply a 'path' value to specify which resource field or array index you are acting on, additionally the move and copy operations require a 'from' which specifies the source field or index. These values must follow the JSON pointer syntax defined in IETF RFC-6901 which we don't recommend that you read because it makes a very simple syntax look very complicated. Instead we have some examples which refer to the following JSON document:

{
  "name": "Homer",
  "age": 36,
  "spouse": {
    "name": "Marge",
    "age": 34
  },
  "children": [
    {
      "name": "Bart",
      "age": 10
    },
    {
       "name": "Lisa",
       "age": 8
    },
    {
      "name": "Maggie",
      "age": 1
    }
  ]
}
Value we want to point atRequired JSON pointerExample PATCH operation usage
Homer (the entire root resource) "" {"op": "replace", "path": "", "value": {"name": "Groundskeeper Willy", "age": 44}}
Homer's age "/age" {"op": "replace", "path": "/age", "value": 36}
A new 'sex' attribute for Homer "/sex" {"op": "add", "path": "/sex", "value": "M"}
Marge "/spouse" {"op": "remove", "path": "/spouse"}
Marge's age "/spouse/age" {"op": "replace", "path": "/spouse/age", "value": 35}
The array of Homer's children "/children" {"op": "remove", "path": "/children"}
Lisa "/children/1" {"op": "remove", "path": "/children/1"}
The end of the children array (to insert a new child) "/children/-" {"op": "add", "path": "/children/-", "value": {"name": "Hugo", "age": 10}}
Bart's age "/children/0/age" {"op": "replace", "path": "/children/0/age", "value": 11}
Have more questions? Submit a request

0 Comments

Please sign in to leave a comment.