Complete implementation of RFC6902 (patch and diff)
npm install rfc6902



Complete implementation of RFC6902 "JavaScript Object Notation (JSON) Patch"
(including RFC6901 "JavaScript Object Notation (JSON) Pointer"),
for creating and consuming application/json-patch+json documents.
Also offers "diff" functionality without using Object.observe.
Simple web app using the browser-compiled version of the code.
``sh`
npm install --save rfc6902
`js`
const rfc6902 = require('rfc6902')
`js`
rfc6902.createPatch({first: 'Chris'}, {first: 'Chris', last: 'Brown'})
//⇒ [ { op: 'add', path: '/last', value: 'Brown' } ]
`js`
const users = [{first: 'Chris', last: 'Brown', age: 20}]
rfc6902.applyPatch(users, [
{op: 'replace', path: '/0/age', value: 21},
{op: 'add', path: '/-', value: {first: 'Raphael', age: 37}},
])applyPatch
The function returns [null, null],
indicating there were two patches, both applied successfully.
The users variable is modified in place; evaluate it to examine the end result:`js`
users
//⇒ [ { first: 'Chris', last: 'Brown', age: 21 },
// { first: 'Raphael', age: 37 } ]
In ES6 syntax:
`js`
import {applyPatch, createPatch} from 'rfc6902'
Using TypeScript annotations for clarity:
The operations in patch are applied to object in-place.patch
Returns a list of results as long as the given .null
If all operations were successful, each item in the returned list will be ..name
If any of them failed, the corresponding item in the returned list will be an Error instance
with descriptive and .message properties.
Returns a list of operations (a JSON Patch) of the required operations to make input equal to output.remove
In most cases, there is more than one way to transform an object into another.
This method is more efficient than wholesale replacement,
but does not always provide the optimal list of patches.
It uses a simple Levenshtein-type implementation with Arrays,
but it doesn't try for anything much smarter than that,
so it's limited to , add, and replace operations.
Optional
diff argument
The optional diff argument allows the user to specify a partial functiondiffAny
that's called before the built-in function.MyObject
For example, to avoid recursing into instances of a custom class, say, :`js`
function myDiff(input: any, output: any, ptr: Pointer) {
if ((input instanceof MyObject || output instanceof MyObject) && input != output) {
return [{op: 'replace', path: ptr.toString(), value: output}]
}
}
const my_patch = createPatch(input, output, myDiff)MyObject
This will short-circuit on encountering an instance of , but otherwise recurse as usual.
`typescript`
interface Operation {
op: 'add' | 'remove' | 'replace' | 'move' | 'copy' | 'test'
from?: string
path?: string
value?: string
}
Different operations use different combinations of from / value;
see JSON Patch (RFC6902) below.
I'm not going to copy & paste my relatively descriptive commit messages into groups here;
rather, these are just the changes that merited major version bumps:
* Short-circuits JSON pointer traversal over the prototype-polluting tokens __proto__, constructor, and prototype. I.e., /a/__proto__/b and /a/b evaluate to the same thing.
This is in violation of the spec,
which makes no special provisions for this idiosyncrasy of the JavaScript language,
but AFAIK there's no way to strictly comply with the spec in JavaScript.
It would probably be more correct to throw an error in those cases,
but this 'solution' feels less disruptive / more in line with workarounds implemented by other libraries.
* Potential performance regression due to consolidating separate compare(a, b): boolean and diff(a, b): Operation[] logic into basically defining compare(a, b) as !diff(a, b).length (i.e., diff(a, b) returns empty array).diff
This simplifies the codebase and ensures underlying semantics do not diverge,
but potentially does unnecessary work in computing a full diff when all we really care about is whether there is at least one difference.
It also facilitates the user completely specifying custom diff functionality with just one function,diff
as opposed to a and corresponding compare
(and avoids the headache of having to propagate both of those around internally).
* Corrects improper behavior in a few buggy edge cases,
which might conceivably break consumers relying on incorrect behavior.
(Tbh that applies to most bugfixes but I felt there were enough to add up to incrementing the major version.)
* Also moves around some of the internal API that was not intended to be used externally,
but technically _was_ exported.
If you're only importing the public API of applyPatch, createPatch, and createTests from 'rfc6902',
nothing has changed.
If you've ever implemented Levenshtein's algorithm,
or played tricks with git rebase to get a reasonable sequence of commits,ab
you'll realize that computing diffs is rarely deterministic.
E.g., to transform the string → bc, you could:a
1. Delete (⇒ b)c
2. and then append (⇒ bc)
_Or..._
1. Replace b with c (⇒ ac)a
2. and then replace with b (⇒ bc)
Both consist of two operations, so either one is a valid solution.
Applying json-patch documents is much easier than generating them,Object.observe()
which might explain why, when I started this project,
there were more than five patch-applying RFC6902 implementations in NPM,
but none for generating a patch from two distinct objects.
(There was one that used , which only works when you're the one making the changes,Object.observe()
and only as long as hasn't been deprecated, which it has.)
So when comparing _your_ data objects, you'll want to ensure that the patches it generates meet your needs.
The algorithm used by this library is not optimal,
but it's more efficient than the strategy of wholesale replacing everything that's not an exact match.
Of course, this only applies to generating the patches.
Applying them is deterministic and unambiguously specified by RFC6902.
The RFC is a quick and easy read, but here's the gist:
* JSON Pointer is a system for pointing to some fragment of a JSON document.
* A pointer is a string that is composed of zero or more /reference-token parts.
- When there are zero (the empty string), the pointer indicates the entire JSON document.
- Otherwise, the parts are read from left to right, each one selecting part of the current document,
and presenting only that fragment of the document to the next part.
* The reference-token bits are usually Object keys,
but may also be (decimal) numerals, to indicate array indices.
E.g., consider the NPM registry:
`js`
{
"_updated": 1417985649051,
"flickr-with-uploads": {
"name": "flickr-with-uploads",
"description": "Flickr API with OAuth 1.0A and uploads",
"repository": {
"type": "git",
"url": "git://github.com/chbrown/flickr-with-uploads.git"
},
"homepage": "https://github.com/chbrown/flickr-with-uploads",
"keywords": [
"flickr",
"api",
"backup"
],
...
},
...
}/_updated
1. : this selects the value of that key, which is just a number: 1417985649051/flickr-with-uploads
2. : This selects the entire object:`
js`
{
"name": "flickr-with-uploads",
"description": "Flickr API with OAuth 1.0A and uploads",
"repository": {
"type": "git",
"url": "git://github.com/chbrown/flickr-with-uploads.git"
},
"homepage": "https://github.com/chbrown/flickr-with-uploads",
"keywords": [
"flickr",
"api",
"backup"
],
...
}
/flickr-with-uploads/name
3. : this effectively applies the /name pointer to the result of the previous item,"flickr-with-uploads"
which selects the string, ./flickr-with-uploads/keywords/1
4. : Array indices start at 0,keywords
so this selects the second item from the array, namely, "api".
#### Rules:
* A pointer, if it is not empty, must always start with a slash;
otherwise, it is an "Invalid pointer syntax" error.
* If a key within the JSON document contains a forward slash character
(which is totally valid JSON, but not very nice),
the / in the desired key should be replaced by the escape sequence, ~1.~
* If a key within the JSON document contains a tilde (again valid JSON, but not very common),
the should be replaced by the other escape sequence, ~0.~1
This allows keys containing the literal string (which is especially cruel)/~01
to be referenced by a JSON pointer (e.g., should return true when applied to the object {"~1":true}).
* All double quotation marks, reverse slashes,
and control characters _must_ escaped, since a JSON Pointer is a JSON string.
* A pointer that refers to a non-existent value counts as an error, too.
But not necessarily as fatal as a syntax error.
#### Example
This project implements JSON Pointer functionality; e.g.:
`js`
const {Pointer} = require('rfc6902')
const repository = {
contributors: ['chbrown', 'diachedelic', 'nathanrobinson', 'kbiedrzycki', 'stefanmaric']
}
const pointer = Pointer.fromJSON('/contributors/0')
//⇒ Pointer { tokens: [ '', 'contributors', '0' ] }
pointer.get(repository)
//⇒ 'chbrown'
The RFC is only 18 pages long, but here are the basics:
A JSON Patch document is a JSON document such that:
* The MIME Type is application/json-patch+json.json-patch
* The file extension is op
* It is an array of patch objects, potentially empty.
* Each patch object has a key, , with one of the following six values,add
and an operator-specific set of other keys.
- : Insert the given value at path. Or replace it, if it already exists.path
If the parent of the intended target does not exist, produce an error.
If the final reference-token of is "-", and the parent is an array, append value to it.path
+ : JSON Pointervalue
+ : JSON objectremove
- : Remove the value at path. Produces an error if it does not exist.path
If refers to an element within an array,path
splice it out so that subsequent elements fill in the gap (decrementing the length of the array).
+ : JSON Pointerreplace
- : Replace the current value at path with value.remove
It's exactly the same as performing a operation and then an add operation on the same path,path
since there _must_ be a pre-existing value.
+ : JSON Pointervalue
+ : JSON objectmove
- : Remove the value at from, and set path to that value.from
There _must_ be a value at , but not necessarily at path;remove
it's the same as performing a operation, and then an add operation, but on different paths.from
+ : JSON Pointerpath
+ : JSON Pointercopy
- : Get the value at from and set path to that value.move
Same as , but doesn't remove the original value.from
+ : JSON Pointerpath
+ : JSON Pointertest
- : Check that the value at path is equal to value.path
If it is not, the entire patch is considered to be a failure.
+ : JSON Pointervalue`: JSON object
+
Copyright 2014-2021 Christopher Brown.
MIT Licensed.