Guide
JSON Patch in Go
How to apply and generate RFC 6902 JSON Patches in Go with the evanphx/json-patch library - the same implementation Kubernetes uses - with working code you can copy straight into a project.
Installing the library
The de-facto standard library is evanphx/json-patch. It implements every RFC 6902 operation - add, remove, replace, move, copy and test:
go get github.com/evanphx/json-patch/v5
Applying a JSON Patch
jsonpatch.DecodePatch parses a patch document, and patch.Apply executes it against a JSON document. Both work on raw []byte:
package main
import (
"fmt"
jsonpatch "github.com/evanphx/json-patch/v5"
)
func main() {
doc := []byte(`{"name": "demo", "version": 1, "tags": ["a", "b"]}`)
patchJSON := []byte(`[
{"op": "replace", "path": "/version", "value": 2},
{"op": "add", "path": "/tags/-", "value": "c"},
{"op": "remove", "path": "/name"}
]`)
patch, err := jsonpatch.DecodePatch(patchJSON)
if err != nil {
panic(err) // the patch document itself is malformed
}
patched, err := patch.Apply(doc)
if err != nil {
panic(err) // an operation failed against this document
}
fmt.Println(string(patched))
// {"tags":["a","b","c"],"version":2}
}Note the special path /tags/-: the dash appends to the end of an array. Paths follow JSON Pointer (RFC 6901), so ~ and / inside keys are escaped as ~0 and ~1.
Generating a patch from two documents
jsonpatch.CreatePatch diffs two documents and returns the RFC 6902 operations that turn the first into the second - the same thing the JSON Patch Generator does in your browser:
original := []byte(`{"name": "demo", "version": 1}`)
modified := []byte(`{"name": "demo", "version": 2, "tags": ["a"]}`)
patchJSON, err := jsonpatch.CreatePatch(original, modified)
if err != nil {
panic(err)
}
fmt.Println(string(patchJSON))
// [{"op":"replace","path":"/version","value":2},{"op":"add","path":"/tags","value":["a"]}]Handling test failures and errors
Two different things can go wrong, and they deserve different handling. DecodePatch fails when the patch itself is invalid JSON or uses an unknown operation. Apply fails when a well-formed patch does not fit the document - a missing path, an array index out of range, or a test operation whose value does not match:
patch, err := jsonpatch.DecodePatch(patchJSON)
if err != nil {
// malformed patch: reject it at the API boundary
return fmt.Errorf("invalid patch: %w", err)
}
patched, err := patch.Apply(doc)
if err != nil {
// e.g. a test operation failed: the document is not
// in the state the caller expected - HTTP 412 territory
return fmt.Errorf("patch rejected: %w", err)
}RFC 6902 stops evaluation at the first failed operation. Whether earlier mutations are rolled back depends on the library, so do not assume transactional behavior without testing the implementation.
Common pitfalls
- Evaluation stops on error. RFC 6902 does not require rollback of earlier operations; verify the library's behavior before relying on transaction-like semantics. Applying a patch twice is not idempotent for
addon arrays. - Numbers are all float64.
encoding/jsonunmarshals numbers tofloat64, so a test on{"value": 1}compares against1.0. The library normalises this, but mixingjson.Numberand floats in your own comparisons bites. - Array indices shift. Removing index 0 makes index 1 become index 0. Order remove operations from the highest index down, or use value-based test guards.
- Apply does not mutate the input. It returns a new
[]byte; assign the result.
Try it in the browser first
Before writing Go, prototype the patch visually. Paste your two documents into the JSON Patch Generator to get the exact RFC 6902 operations, or use the JSON Diff tool to see every changed line. Then drop the generated patch into patch.Apply.
New to the format? Read the JSON Patch documentation or browse real-world examples.