nano-rfc6902
v1.0.12
Published
Lightweight JSON Patch (RFC 6902) utilities for Node.js and the browser
Maintainers
Readme
nano-rfc6902
Lightweight JSON Patch (RFC 6902) utilities for Node.js and the browser.
Highlights:
Zero dependencies
Tiny footprint (size-limit: 3 kB max, currently ~1.93 kB brotli)
Fast diff/patch
ESM + CJS + types
Works in Node.js and modern browsers
Generate patches:
createPatch(oldValue, newValue)Apply patches in-place:
applyPatch(target, patch)
Installation
npm install nano-rfc6902Benchmark (nano-rfc6902 vs rfc6902)
Run:
npm run benchmarkPer-operation benchmark (RFC ops):
npm run benchmark:opsWhat it does:
- Builds this library (
dist) first. - Runs
benchmark/compare.mjswith warmup and repeated iterations. - Prints
diffandpatchthroughput fornano-rfc6902andrfc6902. benchmark:opsprints per-operation throughput forapplyPatch(add,remove,replace,move,copy,test).createPatchis diff-based and benchmarks per-op only where applicable (add,remove,replace);move/copy/testare reported as unsupported by design.
How to compare fairly:
- Use the same machine and Node.js version.
- Run several times and compare medians, not a single run.
- Treat results as workload-specific (your real data shape may differ).
Sample results (May 30, 2026, Node 20, iterations=5000, warmup=500, runs=21 median):
Diff benchmark
nano-rfc6902 | total= 42.66ms | avg= 0.0085ms/op | throughput= 117207 ops/s
rfc6902 | total= 533.67ms | avg= 0.1067ms/op | throughput= 9369 ops/s
Patch benchmark
nano-rfc6902 | total= 34.07ms | avg= 0.0068ms/op | throughput= 146775 ops/s | patch=12
rfc6902 | total= 80.52ms | avg= 0.0161ms/op | throughput= 62096 ops/s | patch=12Speed on this fixture (throughput):
- Diff:
nano-rfc6902is ~12.51x faster thanrfc6902. - Patch:
nano-rfc6902is ~2.36x faster thanrfc6902.
Per-operation sample (npm run benchmark:ops, same env):
applyPatch
add | nano=13025345 ops/s | rfc6902= 892836 ops/s
remove | nano= 3707169 ops/s | rfc6902= 660545 ops/s
replace | nano= 5354850 ops/s | rfc6902=2156075 ops/s
move | nano= 2571448 ops/s | rfc6902= 820362 ops/s
copy | nano= 6669690 ops/s | rfc6902= 990198 ops/s
test | nano= 6751812 ops/s | rfc6902=1754571 ops/s
createPatch
add | nano=2374326 ops/s | rfc6902= 130955 ops/s
remove | nano=2372869 ops/s | rfc6902= 133718 ops/s
replace | nano= 497928 ops/s | rfc6902= 176571 ops/s
move/copy/test: unsupported by createPatch (diff-based)Who is faster (per operation, lower total time over 5000 iterations):
| Operation | API | Faster Library | Speedup | | --- | --- | --- | --- | | add | applyPatch | nano-rfc6902 | ~14.74x | | remove | applyPatch | nano-rfc6902 | ~5.61x | | replace | applyPatch | nano-rfc6902 | ~2.49x | | move | applyPatch | nano-rfc6902 | ~3.14x | | copy | applyPatch | nano-rfc6902 | ~6.73x | | test | applyPatch | nano-rfc6902 | ~3.85x | | add | createPatch | nano-rfc6902 | ~18.10x | | remove | createPatch | nano-rfc6902 | ~17.72x | | replace | createPatch | nano-rfc6902 | ~2.82x |
Quick start
Node.js (ESM)
import { createPatch, applyPatch } from "nano-rfc6902";
const before = { name: "Ada", skills: ["math"] };
const after = { name: "Ada Lovelace", skills: ["math", "programming"] };
// 1) Create a JSON Patch (RFC 6902 operations)
const patch = createPatch(before, after);
// Example patch (shape will depend on diff):
// [
// { op: 'replace', path: '/name', value: 'Ada Lovelace' },
// { op: 'add', path: '/skills/1', value: 'programming' }
// ]
// 2) Apply the patch (mutates the target in-place)
applyPatch(before, patch);
console.log(before); // -> { name: 'Ada Lovelace', skills: ['math', 'programming'] }Node.js (CommonJS)
const { createPatch, applyPatch } = require("nano-rfc6902");
const a = { count: 1 };
const b = { count: 2 };
const patch = createPatch(a, b);
applyPatch(a, patch);
console.log(a); // -> { count: 2 }Browser
With a bundler (Vite, Webpack, etc.), import from the package name:
<script type="module">
import { createPatch, applyPatch } from "nano-rfc6902";
const oldState = { items: ["a"] };
const newState = { items: ["a", "b"] };
const patch = createPatch(oldState, newState);
applyPatch(oldState, patch);
console.log(oldState); // -> { items: ['a', 'b'] }
</script>Without a bundler, you can use a CDN that serves ESM:
<script type="module">
import {
createPatch,
applyPatch,
} from "https://cdn.jsdelivr.net/npm/nano-rfc6902/+esm";
const src = { a: 1 };
const dst = { a: 1, b: 2 };
const patch = createPatch(src, dst);
applyPatch(src, patch);
console.log(patch); // e.g., [{ op: 'add', path: '/b', value: 2 }]
</script>API
createPatch(oldValue, newValue) => Operation[]
Computes a minimal set of RFC 6902 operations to transform oldValue into newValue.
- Diffs primitives, objects, and arrays.
- Arrays use an LCS-based strategy to produce intuitive insert/remove/replace operations and preserve nested diffs when elements are equal by deep comparison.
applyPatch(target, patch) => void
Applies an RFC 6902 patch to target in-place.
- Supports
add,remove,replace,move,copy, andtest. - Uses JSON Pointer (RFC 6901) for
pathandfromfields (e.g.,/a/b/0). - Throws if paths are invalid or
testfails.
Utils: isSafeApply(patch) => boolean
A small utility exported as a separate entry that validates whether a patch only targets object keys (and never array indices or the special - append). Returns true if the patch is safe to apply without mutating array positions. Implementation: TypeScript.isSafeApply()
Import
- ESM:
import { isSafeApply } from "nano-rfc6902/isSafeApply"; - CommonJS:
const { isSafeApply } = require("nano-rfc6902/isSafeApply");
Safe examples (true)
import { isSafeApply } from "nano-rfc6902/isSafeApply";
const patch = [
{ op: "add", path: "/user/name", value: "Ada" },
{ op: "replace", path: "/meta/title", value: "Dr." },
{ op: "test", path: "/count", value: 1 },
];
isSafeApply(patch); // trueUnsafe examples (false)
import { isSafeApply } from "nano-rfc6902/isSafeApply";
// Targets array index
isSafeApply([{ op: "add", path: "/items/0", value: "a" }]); // false
// Appends to array end
isSafeApply([{ op: "add", path: "/items/-", value: "a" }]); // false
// move/copy touching arrays (either from or path)
isSafeApply([{ op: "move", from: "/items/0", path: "/items/1" }]); // false
isSafeApply([{ op: "copy", from: "/a/0", path: "/b/0" }]); // falseNotes
- For
moveandcopy, bothfromandpathmust be object-key paths (no array indices or-). - Empty patches are considered safe.
Types (TypeScript)
This package ships first-class types. Core shapes mirror RFC 6902:
type JSONValue =
| string
| number
| boolean
| null
| undefined
| JSONValue[]
| { [key: string]: JSONValue };
type Operation =
| { op: "add"; path: string; value: JSONValue }
| { op: "remove"; path: string }
| { op: "replace"; path: string; value: JSONValue }
| { op: "move"; from: string; path: string }
| { op: "copy"; from: string; path: string }
| { op: "test"; path: string; value: JSONValue };Notes:
- JSON Pointer escaping follows RFC 6901:
~->~0,/->~1. applyPatchmutates the target you pass in.undefinedis included inJSONValuefor ergonomic diffs in JS/TS; be aware that literal JSON does not haveundefined.
RFC 6902 JSON Patch Overview
RFC 6902 defines a JSON document structure for expressing a sequence of operations to apply to a JSON document. It's commonly used for:
- Efficient API updates (send only changes, not entire documents)
- Real-time collaboration and operational transformation
- Version control and change tracking
- Undo/redo functionality
- Optimistic UI updates
JSON Pointer (RFC 6901)
Operations use JSON Pointer syntax to reference locations in documents:
/- Root document/foo- Property "foo" at root/foo/bar- Nested property "bar" inside "foo"/array/0- First element of array/array/-- Append to end of array (add operation only)
Special characters must be escaped:
~becomes~0/becomes~1
Example: To reference property "a/b~c", use path "/a~1b~0c"
All Operation Types
1. add - Add a value
Adds a value at the specified location. For objects, creates or overwrites the property. For arrays, inserts at the index (shifting elements right).
import { applyPatch } from "nano-rfc6902";
// Add object property
const obj = { name: "Alice" };
applyPatch(obj, [{ op: "add", path: "/age", value: 30 }]);
console.log(obj); // { name: "Alice", age: 30 }
// Add to array at specific index
const arr = ["a", "c"];
applyPatch(arr, [{ op: "add", path: "/1", value: "b" }]);
console.log(arr); // ["a", "b", "c"]
// Append to array end
const items = [1, 2];
applyPatch(items, [{ op: "add", path: "/-", value: 3 }]);
console.log(items); // [1, 2, 3]
// Add nested property (auto-creates intermediate objects)
const data = {};
applyPatch(data, [{ op: "add", path: "/user/profile/name", value: "Bob" }]);
console.log(data); // { user: { profile: { name: "Bob" } } }2. remove - Remove a value
Removes the value at the specified location. For arrays, removes the element and shifts remaining elements left.
import { applyPatch } from "nano-rfc6902";
// Remove object property
const obj = { name: "Alice", age: 30, city: "NYC" };
applyPatch(obj, [{ op: "remove", path: "/age" }]);
console.log(obj); // { name: "Alice", city: "NYC" }
// Remove array element
const arr = ["a", "b", "c", "d"];
applyPatch(arr, [{ op: "remove", path: "/1" }]);
console.log(arr); // ["a", "c", "d"]
// Remove nested property
const data = { user: { profile: { name: "Bob", email: "[email protected]" } } };
applyPatch(data, [{ op: "remove", path: "/user/profile/email" }]);
console.log(data); // { user: { profile: { name: "Bob" } } }3. replace - Replace a value
Replaces the value at the specified location. Equivalent to remove followed by add, but atomic.
import { applyPatch } from "nano-rfc6902";
// Replace object property
const obj = { name: "Alice", status: "pending" };
applyPatch(obj, [{ op: "replace", path: "/status", value: "active" }]);
console.log(obj); // { name: "Alice", status: "active" }
// Replace array element
const arr = [1, 2, 3];
applyPatch(arr, [{ op: "replace", path: "/1", value: 99 }]);
console.log(arr); // [1, 99, 3]
// Replace nested value
const config = { server: { port: 3000, host: "localhost" } };
applyPatch(config, [{ op: "replace", path: "/server/port", value: 8080 }]);
console.log(config); // { server: { port: 8080, host: "localhost" } }4. move - Move a value
Removes the value at from location and adds it to path location. Atomic operation.
import { applyPatch } from "nano-rfc6902";
// Move property between objects
const obj = { temp: { value: 42 }, data: {} };
applyPatch(obj, [{ op: "move", from: "/temp/value", path: "/data/value" }]);
console.log(obj); // { temp: {}, data: { value: 42 } }
// Move array element
const arr = ["a", "b", "c", "d"];
applyPatch(arr, [{ op: "move", from: "/3", path: "/0" }]);
console.log(arr); // ["d", "a", "b", "c"]
// Rename property
const user = { firstName: "Alice", lastName: "Smith" };
applyPatch(user, [{ op: "move", from: "/firstName", path: "/name" }]);
console.log(user); // { name: "Alice", lastName: "Smith" }5. copy - Copy a value
Copies the value at from location to path location. Creates a deep clone.
import { applyPatch } from "nano-rfc6902";
// Copy object property
const obj = { original: { value: 42 }, backup: {} };
applyPatch(obj, [
{ op: "copy", from: "/original/value", path: "/backup/value" },
]);
console.log(obj); // { original: { value: 42 }, backup: { value: 42 } }
// Copy array element
const arr = [{ id: 1, name: "Alice" }];
applyPatch(arr, [{ op: "copy", from: "/0", path: "/-" }]);
console.log(arr); // [{ id: 1, name: "Alice" }, { id: 1, name: "Alice" }]
// Duplicate nested structure
const data = { template: { x: 1, y: 2 } };
applyPatch(data, [{ op: "copy", from: "/template", path: "/instance" }]);
console.log(data); // { template: { x: 1, y: 2 }, instance: { x: 1, y: 2 } }
// Modifications to copy don't affect original
data.instance.x = 99;
console.log(data.template.x); // Still 16. test - Test a value
Tests that the value at the specified location equals the given value. Throws an error if the test fails. Useful for preventing conflicts in concurrent updates.
import { applyPatch } from "nano-rfc6902";
// Successful test
const obj = { version: 1, data: "hello" };
applyPatch(obj, [
{ op: "test", path: "/version", value: 1 },
{ op: "replace", path: "/data", value: "world" },
]);
console.log(obj); // { version: 1, data: "world" }
// Failed test throws error
const user = { age: 25 };
try {
applyPatch(user, [
{ op: "test", path: "/age", value: 30 }, // Expects 30, but actual is 25
{ op: "replace", path: "/age", value: 31 },
]);
} catch (err) {
console.log(err.message); // "Test operation failed at path /age"
}
// Test with nested objects
const config = { settings: { theme: "dark", lang: "en" } };
applyPatch(config, [
{ op: "test", path: "/settings/theme", value: "dark" },
{ op: "replace", path: "/settings/theme", value: "light" },
]);
console.log(config); // { settings: { theme: "light", lang: "en" } }Complex Example: Multiple Operations
import { applyPatch } from "nano-rfc6902";
const document = {
users: [
{ id: 1, name: "Alice", role: "admin" },
{ id: 2, name: "Bob", role: "user" },
],
metadata: {
version: 1,
lastModified: "2024-01-01",
},
};
applyPatch(document, [
// Test version before applying changes
{ op: "test", path: "/metadata/version", value: 1 },
// Update user role
{ op: "replace", path: "/users/1/role", value: "admin" },
// Add new user
{
op: "add",
path: "/users/-",
value: { id: 3, name: "Charlie", role: "user" },
},
// Update metadata
{ op: "replace", path: "/metadata/lastModified", value: "2024-01-15" },
{ op: "replace", path: "/metadata/version", value: 2 },
// Add new metadata field
{ op: "add", path: "/metadata/author", value: "System" },
]);
console.log(document);
// {
// users: [
// { id: 1, name: "Alice", role: "admin" },
// { id: 2, name: "Bob", role: "admin" },
// { id: 3, name: "Charlie", role: "user" }
// ],
// metadata: {
// version: 2,
// lastModified: "2024-01-15",
// author: "System"
// }
// }Creating Patches Automatically
Instead of manually writing patches, use createPatch to generate them:
import { createPatch, applyPatch } from "nano-rfc6902";
const before = {
name: "Alice",
age: 30,
hobbies: ["reading", "gaming"],
address: { city: "NYC", zip: "10001" },
};
const after = {
name: "Alice",
age: 31,
hobbies: ["reading", "gaming", "hiking"],
address: { city: "NYC", zip: "10002", country: "USA" },
};
// Generate patch automatically
const patch = createPatch(before, after);
console.log(patch);
// [
// { op: "replace", path: "/age", value: 31 },
// { op: "add", path: "/hobbies/2", value: "hiking" },
// { op: "replace", path: "/address/zip", value: "10002" },
// { op: "add", path: "/address/country", value: "USA" }
// ]
// Apply to original
applyPatch(before, patch);
console.log(before); // Now matches 'after'Development
Prerequisites
- Node.js >= 20.0.0 (LTS)
- npm or pnpm
Install dependencies
npm installBuild
npm run buildOutputs:
dist/index.js— ES moduledist/index.cjs— CommonJSdist/index.d.ts— TypeScript declarations
Type checking
npm run type-checkTests
npm testSize check
npm run sizeCurrent size-limit target and result:
- Limit:
3 kB(dist/index.js) - Measured:
~1.93 kB(minified + brotli)
Features
- Zero dependencies
- Tiny footprint: 3 kB max (size-limit target), currently ~1.93 kB min+brotli
- Fast and efficient diff/patch
- JSON Patch (RFC 6902) create/apply
- Small API surface
- TypeScript types included
- Works in Node.js and modern browsers
- ESM and CJS builds
License
BSD-3-Clause
