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 add on arrays.
  • Numbers are all float64. encoding/json unmarshals numbers to float64, so a test on {"value": 1} compares against 1.0. The library normalises this, but mixing json.Number and 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.