Documentation

JSON Patch and Arrays

Arrays are where JSON Patch (RFC 6902) trips most developers up. Every array rule - appending with /-, inserting at an index, removing without off-by-one bugs - with examples you can run in the tool.

To append a value to a JSON array with JSON Patch, use the add operation with a path ending in /-: { "op": "add", "path": "/items/-", "value": "new item" }. To insert at a position, use the numeric index. To delete, use remove with the index - and remember every later element shifts one position left.

The rules in one minute

Arrays are addressed by zero-based numeric indexes inside a JSON Pointer path. Four rules cover almost everything:

  • add at an index inserts before that position - it never overwrites.
  • add at /- appends to the end of the array.
  • remove at an index deletes that element and shifts every later element one position left.
  • Operations apply in order, so each operation sees the array left behind by the previous one.

Append an item: the /- path

Patch

[
  { "op": "add", "path": "/items/-", "value": "new item" }
]

Before

{ "items": ["a", "b"] }

After

{ "items": ["a", "b", "new item"] }

The - token comes from JSON Pointer (RFC 6901): it names the position one past the last element of an array. Because it does not depend on the current length, it is the safest way to append - you never need to count elements first.

Insert at a specific index

Patch

[
  { "op": "add", "path": "/items/1", "value": "inserted" }
]

Before

{ "items": ["a", "b", "c"] }

After

{ "items": ["a", "inserted", "b", "c"] }

The new value lands before whatever was at that index, and everything from that position on shifts one place right. The index may range from 0 up to the array length: using the length itself appends, exactly like /-. An index greater than the length is an error.

Empty arrays and missing parents

On an empty array, /items/0 and /items/- both work and both mean "first element". But the array itself must exist: add /items/- fails if the document has no items key at all. Create the container first:

Patch

[
  { "op": "add", "path": "/items", "value": [] },
  { "op": "add", "path": "/items/-", "value": "first" }
]

Replace an element (or the whole array)

replace changes the value at an exact path. Aimed at an index it swaps one element; aimed at the array itself it swaps everything:

Patch

[
  { "op": "replace", "path": "/items/1", "value": "B" }
]

Before

{ "items": ["a", "b", "c"] }

After

{ "items": ["a", "B", "c"] }

Remove an element - and the index shift

Patch

[
  { "op": "remove", "path": "/items/1" }
]

Before

{ "items": ["a", "b", "c"] }

After

{ "items": ["a", "c"] }

After the removal, c moves from index 2 to index 1. This is the single most common source of JSON Patch bugs: an index that was correct when you wrote the patch may point somewhere else after an earlier operation runs.

Removing several elements in one patch

Because operations run in order and each removal shifts the tail left, the safe pattern is to remove from the highest index to the lowest. To delete "b" and "d" from ["a", "b", "c", "d"]:

Patch - works

[
  { "op": "remove", "path": "/items/3" },
  { "op": "remove", "path": "/items/1" }
]

Patch - removes the wrong elements

[
  { "op": "remove", "path": "/items/1" },
  { "op": "remove", "path": "/items/3" }
]

The second patch fails: after removing index 1 the array has only 3 elements, so index 3 no longer exists. Removing the same index twice is the other classic trap - it deletes two different elements, not the same one twice.

Why there are no wildcards

RFC 6902 has no wildcard or "all elements" syntax. A path segment is either an exact object key or an exact array index - /items/* looks for a key literally named * and fails on an array. To change every element, the patch must contain one operation per element:

Patch

[
  { "op": "replace", "path": "/items/0/done", "value": true },
  { "op": "replace", "path": "/items/1/done", "value": true },
  { "op": "replace", "path": "/items/2/done", "value": true }
]

Generating one operation per element is exactly what a diff does - paste your before and after documents into the generator and it emits every operation for you. If you truly need "update all matching elements" semantics, that is a loop in your application code producing a patch, not something a single patch can express.

Arrays of objects and reordering

Paths keep walking through array elements into the objects inside them: /users/0/email is the email of the first user. And move reorders elements: moving /users/0 to /users/2 takes the first user out and inserts them at position 2. Remember that move removes first and inserts second, so the destination index applies to the array after the removal.

Patch

[
  { "op": "replace", "path": "/users/0/email", "value": "new@example.com" },
  { "op": "move", "from": "/users/0", "path": "/users/2" }
]

Guard array writes with test

Because indexes move, patches on arrays are fragile when the document can change between generation and application. The test operation stops evaluation if reality does not match your assumption:

Patch

[
  { "op": "test", "path": "/items/1", "value": "b" },
  { "op": "remove", "path": "/items/1" }
]

If /items/1 is no longer "b", nothing is removed. For APIs this is optimistic concurrency control built into the format.

Common mistakes

Removing several elements from low to high index

Each remove shifts everything after it one position left. To remove "b" and "d" from ["a","b","c","d"], remove /items/3 first, then /items/1 - or recount after every remove.

Expecting add to overwrite an existing element

add never overwrites an array element: it inserts before the index you give. To change an element in place, use replace.

Using /- with remove, replace or test

"-" names a position one past the last element. It only makes sense with add. remove /items/- fails because there is no element there to remove.

Writing array indexes with leading zeros

/items/01 is invalid. RFC 6902 array indexes are base-10 without leading zeros: /items/1.

Trying to address all elements at once

There is no wildcard syntax. /items/* looks for an object key literally named "*". Emit one operation per element instead - the generator does this for you.

Reference

Array behavior is defined by RFC 6902, section 4.1 (add) and section 4.2 (remove). The - token and index syntax come from RFC 6901, section 4.

Try it on a real array

Open the tool with an array example preloaded - generate the append patch yourself, or apply a removal.