Structural diff and patch for JSON values in Go, using JSON-Pointer (RFC 6901) paths and JSON-Patch-style (RFC 6902) operation names. Zero third-party dependencies — standard library only.
go-jsondiff operates on values already unmarshaled from JSON via
encoding/json into any (objects become map[string]any, arrays become
[]any, scalars become string / float64 / bool / nil). Diff
computes the list of changes between two such values; ApplyPatch re-applies
that list to reconstruct one value from the other.
go get github.com/kasapdev/go-jsondiff
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/kasapdev/go-jsondiff"
)
func main() {
before := `{
"name": "Alice",
"age": 30,
"tags": ["admin", "staff"],
"address": {"city": "Istanbul"}
}`
after := `{
"name": "Alice",
"age": 31,
"tags": ["admin"],
"address": {"city": "Ankara", "country": "TR"}
}`
var a, b any
if err := json.Unmarshal([]byte(before), &a); err != nil {
log.Fatal(err)
}
if err := json.Unmarshal([]byte(after), &b); err != nil {
log.Fatal(err)
}
changes := jsondiff.Diff(a, b)
fmt.Println("changes:")
for _, c := range changes {
fmt.Println(" ", c)
}
patched, err := jsondiff.ApplyPatch(changes, a)
if err != nil {
log.Fatal(err)
}
out, err := json.MarshalIndent(patched, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println("patched result:")
fmt.Println(string(out))
}Running this prints the computed changes (e.g. ~ /age: 30 -> 31,
~ /address/city: Istanbul -> Ankara, + /address/country = TR,
- /tags/1 (was staff)), followed by the reconstructed document, which is
deep-equal to after.
Walks two JSON-like values recursively and returns every difference between
them as a Change:
- A key/index present in
bbut notais reported asadd. - A key/index present in
abut notbis reported asremove. - A key/index present in both, whose values differ (including differing
JSON types, e.g. a string becoming a number), is reported as
replace.
Object keys are compared by name and visited in sorted order, so Diff is
deterministic. Arrays are compared positionally over their common-length
prefix (so an element changed at index i is reported at .../i), with any
extra trailing elements reported as add or remove. This is a simple,
correct, index-based array diff rather than a minimal/LCS-based one — it
does not always produce the smallest possible change set for edits in the
middle of an array, but ApplyPatch(Diff(a, b), a) is always equal to b.
type Change struct {
Op Op // "add", "remove", or "replace"
Path string // RFC 6901 JSON-Pointer, e.g. "/address/city" or "/tags/0"
OldValue any // set for "remove" and "replace"
NewValue any // set for "add" and "replace"
}Path is an RFC 6901 JSON-Pointer identifying the changed location; the
document root itself is addressed by the empty string "". Object keys are
escaped per RFC 6901 (~ becomes ~0, / becomes ~1). Change also
implements String() string for a compact, human-readable rendering (e.g.
~ /age: 30 -> 31), intended for logging/debugging rather than as a
machine-readable format.
Applies changes (as produced by Diff) to a deep copy of before and
returns the resulting value; before itself is never mutated. Changes are
applied in order. Adding into an array inserts at the given index (shifting
later elements up); removing or replacing requires the path to already
resolve in the document. ApplyPatch returns a non-nil error — rather
than panicking — for a malformed JSON-Pointer or a path that cannot be
resolved against the current document (e.g. removing a key that isn't
present, or replacing an out-of-range array index).
go test ./...
The test suite includes table-driven unit tests for Diff covering added
keys, removed keys, changed scalars, changed nested values, and array
element addition/removal/replacement by index; a required round-trip suite
proving ApplyPatch(Diff(a, b), a) reconstructs b across several
nontrivial nested fixtures (nested objects, array element additions and
removals, and type changes such as string-to-number and object-to-array);
and error-path tests confirming ApplyPatch returns an error instead of
panicking on malformed or unresolvable patches.
MIT — see LICENSE.