Skip to content

About

Structural JSON diff and patch with JSON-Pointer paths for Go. Zero-dependency Go stdlib-only library.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

go-jsondiff

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.

Installation

go get github.com/kasapdev/go-jsondiff

Usage

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.

API

func Diff(a, b any) []Change

Walks two JSON-like values recursively and returns every difference between them as a Change:

  • A key/index present in b but not a is reported as add.
  • A key/index present in a but not b is reported as remove.
  • 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

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.

func ApplyPatch(changes []Change, before any) (any, error)

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).

Testing

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.

License

MIT — see LICENSE.

About

Structural JSON diff and patch with JSON-Pointer paths for Go. Zero-dependency Go stdlib-only library.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages