Skip to content

JSON Patch RFC issues

datadata uses JSON Patch (RFC 6902) as its structured change format. The RFC was designed for HTTP PATCH — one writer, one round-trip — and applying it in a concurrent, multi-writer engine surfaces real problems. This page collects the ones we’ve hit.

Array operations address positions, not items

Section titled “Array operations address positions, not items”

The issue. RFC 6902 paths address array elements by index (/items/3). An index is only meaningful against the exact array the patch was computed from: any concurrent insert or remove earlier in the array silently retargets every following operation. The patch still applies — to the wrong elements. The - append token has the mirror problem: two concurrent appends both “succeed” with no way to express ordering intent between them.

A sibling problem hits objects too: add and replace of a container replace the whole container — JSON Patch does not deep-merge. Two clients concurrently mutating entries of the same nested object produce patches that target the same container path, and the later write clobbers the earlier one even though the edits were logically independent.

datadata’s workaround. Collections that matter are neither arrays nor nested trees. They’re flat records keyed by stable ids with fractional-index ordering — so “insert between A and B” and “update item X” are single-key operations at disjoint leaf paths, which concurrent edits can’t retarget or clobber. This works well, but it’s a modeling convention the RFC forces on us, not something the patch format helps with — and it’s load-bearing enough that every mutable collection the engine itself maintains (the index, schema migration logs, session changesets) follows it.

The issue. The auto-generated test guards cover replace and remove — operations that have a prior value to assert. An add of a previously-absent member has nothing to test: the RFC offers no way to say “this key must not exist yet.” So two clients concurrently adding the same new key both pass a patch guard, and the later write silently overwrites the earlier one. Guarded writes are a complete same-path conflict detector for changes and deletions, but not for additions — and every layer built on guards (including session conflict detection) inherits that hole.

datadata’s position. Open. Candidate fixes: extend the patch profile with an explicit absence test, hash-based value preconditions, or telling strict callers to use the coarse sequence guard (which does catch it). For id-keyed records with generated ids the collision is improbable by construction, which is why this is a sharp edge rather than a daily wound.

test is the only concurrency tool, and it’s blunt

Section titled “test is the only concurrency tool, and it’s blunt”

The issue. The RFC’s only precondition mechanism is the test op: assert a value, fail the whole patch otherwise. There is no way to express intent (“increment”, “insert after X”, “set if unset”), so any concurrent touch of a tested value rejects the entire patch — even when the writes were trivially compatible.

datadata’s workaround. Guard modes: auto-generated test ops scoped to the values actually changed (tolerating disjoint edits), or whole-document sequence guards when strictness is wanted. Rejection is treated as a benign, expected signal with a retry path — and the session client turns it into reviewable three-way conflicts. But the underlying expressiveness gap is the RFC’s.

The issue. Patches produced by diffing two states (the common way to get them) are not unique — a move is indistinguishable from a remove+add, a small edit inside a string is a whole-value replace. Whatever the differ guesses becomes the recorded “change”, which degrades both conflict detection (coarser test ops than the actual edit warranted) and the event log’s value as history.

datadata’s position. Live where it’s tolerable (structured fields are small), escape where it isn’t (Yjs for text, where intent-per-keystroke is exactly what the CRDT captures).


Further issues — including ones around move/copy semantics and patch composability — are queued to be written up. If you’ve fought this RFC in your own engine, compare notes with us.