Files
clean-architecture-backend-…/docs/web/patch-contract.md
T

3.4 KiB

Web patch: the client contract

Two patch formats, and they are not interchangeable.

Choosing one

A route accepts exactly one, and sending the other is a 415 rather than a best-effort guess. The reason is that a mismatch is silent in the worst direction:

  • {"a": null} as a merge patch deletes a. As a JSON Patch it is not a patch at all — it is an object where an array was required.
  • A JSON Patch array read as a merge patch is a document whose fields are array indices. It merges nothing and reports success.
Merge patch JSON Patch
Media type application/merge-patch+json application/json-patch+json
RFC 7396 6902
Shape A partial document An ordered array of operations
Delete "field": null {"op":"remove","path":"/field"}
Arrays Replaced whole Addressable per index
Preconditions If-Match only If-Match, plus per-field test

Use merge patch for "change these fields". Use JSON Patch when you need array element edits or a precondition on one field that If-Match cannot express.

What is refused

Fields outside the allowlist. The server declares which fields a merge patch may modify and which pointers a JSON Patch may address. Anything else is refused, and every refused path is named — you will not discover them one round trip at a time.

The allowlist is checked before anything is applied. A refusal tells you nothing about the current values, which is deliberate: deciding afterwards whether a refused field "actually changed anything" would answer a question about data you may not read.

Pointer permission does not travel upwards. Permission on /profile covers /profile/displayName; permission on /profile/displayName does not cover /profile, because replacing the parent deletes every sibling.

A move needs permission on both ends. Checking only the destination would let you relocate data out of a field you may not touch.

Limits. At most 100 operations, pointer depth at most 16, merge-patch nesting depth at most 32. The first two multiply — each operation walks its pointer over a document the server deep-copied first.

Atomicity

A JSON Patch document applies entirely or not at all. Every operation runs against a working copy; the resource is not touched until all of them have succeeded and the result has passed full validation.

This is what makes test useful. A client putting a test first is relying on nothing after it having happened when the test fails, and that reliance holds:

[
  {"op": "test",    "path": "/version",     "value": 4},
  {"op": "replace", "path": "/displayName", "value": "new"}
]

A failed test is 409, not 400: the document was well-formed and permitted, and the resource simply was not in the state you expected. Re-read and retry. The response names the pointer that failed and does not return the server's value — returning it would make test a read primitive for fields you may not read.

Validation

The patched result is validated as a whole document, not field by field. A patch whose individual fields are all valid can produce an object that is not — two fields that must agree, a state transition that is not allowed — and validating only what changed sees none of it.

Preconditions

Patch routes require If-Match. A partial update against a resource that moved underneath you applies your changes to a version you never saw.