fast-json-patch
fast-json-patch is a dependency-free JavaScript implementation of JSON Patch, the RFC 6902 format for describing changes to JSON documents. It applies add, remove, replace, move, copy, and test operations, validates patches, compares two documents to create a patch, and can observe object mutations to generate operations. It supports CommonJS and ES modules and includes TypeScript declarations. Its defaults mutate the input document, validation is opt-in, and prototype modifications are blocked unless a caller deliberately disables that protection.
Use fast-json-patch when RFC 6902 is a real interoperability requirement and wrap it with authorization plus validation. Do not introduce JSON Patch just to avoid writing a small update object, and weigh the stale release before adopting it in new code.
Use it if
- Your API already speaks RFC 6902 and you need to apply or produce interoperable JSON Patch arrays
- You need compact document updates with test operations for optimistic concurrency
- You want to compare two JSON-compatible values or observe a local object and generate patches
- You need a zero-dependency implementation that works in Node and browsers
- Your update shape is under your control and simple fields are enough: a plain partial object is easier to validate, authorize, log, and explain than JSON Pointer plus six operation types
- You might apply client patches without schema-level authorization: structural validation cannot decide whether a user is allowed to replace /role or remove /billing/account
- You want actively shipping releases and current packaging guidance: npm 3.1.1 dates to March 2022, and the README still discusses Node 12 experimental modules and contains contradictory validate and generate signatures in examples
- You need semantic diffs for text, arrays, or domain objects: RFC 6902 describes structural edits and array indexes, which can yield noisy or fragile patches after reordering
Setup reality
npm install fast-json-patch adds no runtime dependencies and the package contains CommonJS, ESM, browser builds, and declarations. The annoying work is not installation but policy. Validate every untrusted patch, keep prototype protection enabled, authorize allowed pointer paths, decide whether mutation is acceptable, clone patch values when references must not be shared, and handle failed test operations as conflicts.
Patterns
Apply several JSON Patch operationsapply-patch
import { applyPatch } from 'fast-json-patch'
const document = { name: 'Ada', tags: ['math'] }
const patch = [
{ op: 'replace', path: '/name', value: 'Ada Lovelace' },
{ op: 'add', path: '/tags/-', value: 'programming' },
]
const result = applyPatch(document, patch, true)
console.log(result.newDocument)The third argument enables validation. By default the original document is mutated and - appends to an array.
Return a patched cloneapply-immutably
import { applyPatch, deepClone } from 'fast-json-patch'
const result = applyPatch(
document,
deepClone(patch),
true,
false,
)
const updated = result.newDocumentThe fourth argument disables document mutation. Clone the patch too when its value objects must not become shared references.
Validate before applying untrusted inputvalidate-patch
import { validate } from 'fast-json-patch'
const error = validate(patch, currentDocument)
if (error) {
throw new Error(`invalid patch at operation ${error.index}: ${error.name}`)
}validate returns one JsonPatchError or undefined, despite an older README example that treats the result like an array.
Use test for optimistic concurrencyguard-with-test
const patch = [
{ op: 'test', path: '/version', value: 7 },
{ op: 'replace', path: '/title', value: 'Revised title' },
{ op: 'replace', path: '/version', value: 8 },
]
const updated = applyPatch(document, patch, true, false).newDocumentA failed test throws TEST_OPERATION_FAILED. Translate that into a conflict response instead of a generic server error.
Create a patch from two documentscompare-documents
import { compare } from 'fast-json-patch'
const before = { user: { name: 'Ada', active: true } }
const after = { user: { name: 'Ada Lovelace', active: true } }
const patch = compare(before, after, true)The third argument adds test operations before destructive changes, producing a patch that detects stale source data.
Generate patches from object mutationsobserve-mutations
import { observe, generate, unobserve } from 'fast-json-patch'
const state = { count: 0, labels: [] }
const observer = observe(state)
state.count += 1
state.labels.push('new')
const patch = generate(observer, true)
unobserve(state, observer)generate takes the observer, not the document. Always unobserve long-lived objects when tracking is finished.
Apply one operationapply-single-operation
import { applyOperation } from 'fast-json-patch'
const result = applyOperation(
document,
{ op: 'remove', path: '/temporary' },
true,
false,
)
console.log(result.removed, result.newDocument)remove fails validation when the path does not exist; it is not an idempotent delete.
Escape a dynamic JSON Pointer segmentescape-pointer-path
import { escapePathComponent } from 'fast-json-patch'
const fieldName = 'settings/theme~dark'
const path = `/profile/${escapePathComponent(fieldName)}`
const operation = { op: 'replace', path, value: true }JSON Pointer encodes ~ as ~0 and / as ~1. Never concatenate an unescaped user-supplied property name.
Move an array elementmove-array-item
const patch = [
{ op: 'move', from: '/items/3', path: '/items/1' },
]
const reordered = applyPatch(document, patch, true, false).newDocumentArray pointers are indexes at apply time, so concurrent insertions can make a previously created reorder patch target the wrong item.
Restrict writable paths before applicationauthorize-paths
const allowedRoots = ['/profile/name', '/profile/timezone']
for (const operation of patch) {
const paths = [operation.path, operation.from].filter(Boolean)
if (paths.some(path => !allowedRoots.includes(path))) {
throw new Error('patch path is not allowed')
}
}
const updated = applyPatch(document, patch, true, false).newDocumentRFC validation checks structure, not authorization. Check both path and from so move or copy cannot read a protected field.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| rfc6902 | npm | You want another standards-focused implementation with a more recently published major release |
| json8-patch | npm | You want an immutable-oriented JSON Patch implementation and JSON Pointer helpers |
| jsondiffpatch | npm | You need human-readable object and text diffs with patch and unpatch, not strict RFC 6902 interchange |