SearchCtrlK

  • ↑↓navigate
  • ↵open
  • escclose

Can’t find it? Contact us

@sdxc/json-patch

Apply, validate and diff RFC 6902 JSON Patch documents

Terminal
npm install @sdxc/json-patch
Terminal
pnpm add @sdxc/json-patch
Terminal
yarn add @sdxc/json-patch
Terminal
bun add @sdxc/json-patch
Used by
sdxc-api

Apply, validate and diff RFC 6902 JSON Patch documents.

A JSON Patch is an ordered list of operations — add, remove, replace, move, copy and test — each aimed at an RFC 6901 JSON Pointer. A patch applies all or nothing: when one operation fails, the document is left exactly as it was.

It complements @sdxc/merge-patch. A merge patch sends a partial document, which is the simplest update to write but cannot set a member to null, edit one array element or make a change conditional. JSON Patch sends operations, so it inserts and removes array elements, writes an explicit null, and guards an update with test preconditions.

Installation

Terminal
npm add @sdxc/json-patch

Fallible calls return @sdxc/result values.

Usage

Apply a patch

typescript
import { apply } from "@sdxc/json-patch";

apply({ title: "Draft", tags: ["a"] }, [
	{ op: "test", path: "/title", value: "Draft" },
	{ op: "replace", path: "/title", value: "Hello" },
	{ op: "add", path: "/tags/-", value: "b" },
]);
// success({ title: "Hello", tags: ["a", "b"] })

Compute a patch

typescript
import { diff } from "@sdxc/json-patch";

diff({ tags: ["a", "c"], note: "x" }, { tags: ["a", "b", "c"], note: null });
// [
//   { op: "add", path: "/tags/1", value: "b" },
//   { op: "replace", path: "/note", value: null },
// ]

Read a request body

typescript
import { parse } from "@sdxc/json-patch";
import { isFailure } from "@sdxc/result";

let patch = parse(await request.text());
if (isFailure(patch)) return new Response(patch.error.message, { status: 400 });

Command line

Terminal
bunx @sdxc/json-patch --help

With @sdxc/kit installed, sdxc pkg json-patch … runs the same command line, using the version your project installs.

apply prints the patched document and exits 1 naming the operation whose path or test failed, diff prints the patch between two documents, and validate exits 1 when a file is not a patch. A - or missing document argument reads stdin.

Terminal
bunx @sdxc/json-patch apply patch.json article.json > patched.json
bunx @sdxc/json-patch diff before.json after.json --compact
bunx @sdxc/json-patch validate patch.json --json

API

apply(document: JSONValue, patch: readonly Operation[]): Result<JSONValue, JsonPatchError>

Runs the operations in order against a copy of document and returns the patched copy, which shares no object or array with either input. The patch is validated first, even when typed, so a value cast from untrusted JSON is still checked before anything runs.

diff(source: JSONValue, target: JSONValue): Operation[]

A patch for which apply(source, patch) returns target. Objects compare member by member regardless of order, an array keeps its shared leading and trailing elements and edits only the span between them, and equal documents give [].

validate(value: unknown): Result<Operation[], JsonPatchError>

Checks that a value is a patch document — an array of operations with a known op, JSON Pointer path and from members, and a value member where the operation needs one — and returns the operations without any unrecognized members.

parse(text: string): Result<Operation[], JsonPatchError>

JSON.parse followed by validate.

stringify(patch: readonly Operation[]): string

Writes a patch as the JSON text a request body carries.

MEDIA_TYPE

"application/json-patch+json".

JsonPatchError

Every failure. index is the failing operation's position, or null when the document as a whole is not a patch, and reason picks the RFC 5789 answer:

reasonMeaningStatus
"invalid-patch"The text or an operation is malformed400
"unresolvable"A path or from has nothing to act on422
"test-failed"A test operation's value did not match409

Types

Operation

The union of AddOperation, RemoveOperation, ReplaceOperation, MoveOperation, CopyOperation and TestOperation. JSONPatch is Operation[].

JSONValue / JSONObject

The JSON values every function reads and writes.

JsonPatchErrorReason

"invalid-patch" | "unresolvable" | "test-failed".

Pattern: Accept JSON Patch On A PATCH Endpoint

The handler checks the media type, applies the patch to the stored resource and maps each failure reason to its status.

typescript
import { apply, MEDIA_TYPE, parse } from "@sdxc/json-patch";
import { isFailure } from "@sdxc/result";

const STATUS = { "invalid-patch": 400, unresolvable: 422, "test-failed": 409 } as const;

async function updateArticle(request: Request, id: string) {
	if (request.headers.get("Content-Type")?.split(";")[0]?.trim() !== MEDIA_TYPE) {
		return new Response(null, { status: 415, headers: { "Accept-Patch": MEDIA_TYPE } });
	}

	let patch = parse(await request.text());
	if (isFailure(patch)) return new Response(patch.error.message, { status: 400 });

	let next = apply(await articles.find(id), patch.data);
	if (isFailure(next)) {
		return new Response(next.error.message, { status: STATUS[next.error.reason] });
	}

	await articles.save(id, next.data);
	return Response.json(next.data);
}

Pattern: Update Only If Nobody Else Did

A test operation turns a patch into a compare-and-set: the update applies only while the member still holds the value the client last saw.

typescript
import { MEDIA_TYPE, stringify } from "@sdxc/json-patch";

let response = await fetch(`/articles/${id}`, {
	method: "PATCH",
	headers: { "Content-Type": MEDIA_TYPE },
	body: stringify([
		{ op: "test", path: "/revision", value: 7 },
		{ op: "replace", path: "/revision", value: 8 },
		{ op: "remove", path: "/tags/2" },
	]),
});
if (response.status === 409) {
	/** Someone else saved first: reload and retry. */
}

Pattern: Send Only What Changed

typescript
import { diff, MEDIA_TYPE, stringify } from "@sdxc/json-patch";

let patch = diff(original, edited);
if (patch.length > 0) {
	await fetch(`/articles/${id}`, {
		method: "PATCH",
		headers: { "Content-Type": MEDIA_TYPE },
		body: stringify(patch),
	});
}
API

Can’t find it? Contact us