wI2L/jsondiff: turning two JSON documents into an RFC6902 patch
Compute the diff between two JSON documents as a series of JSON Patch (RFC6902) operations
At a glance
- What is it?
- A Go package that computes the difference between a source and a target JSON document and returns it as JSON Patch operations, which is what a Kubernetes mutating webhook needs in its response. The library also carries a merge-patch implementation and can apply a patch back to a document.
- Who is it for?
- Adopt jsondiff if you serve a Kubernetes mutating webhook or otherwise need to describe the change between two JSON documents as RFC6902 operations, since it removes the need to hand roll that comparison. Do not adopt it if you need RFC6902 semantics on arbitrary inputs where array element reordering matters, because the comparison walks objects and arrays by position and position based moves are where patch semantics get subtle.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 96 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the library actually returns
jsondiff is a Go package for computing the diff between two JSON documents as a series of RFC6902 operations. That is the whole promise, and the naming of the return type is misleading at first: the result is not a textual diff, it is a list of patch operations that can be applied to the source to obtain the target.
The operations use the standard op, path and value shape. The readme's Kubernetes example ends with output like this:
[{
"op": "replace",
"path": "/spec/containers/0/image",
"value": "nginx:1.19.5-alpine"
}, {
"op": "remove",
"path": "/spec/volumes/0/emptyDir/medium"
}]That is directly usable as the patch payload of a Kubernetes admission webhook response, which is the case the author had in mind, and it is the reason the package exists. The alternative in a webhook is to send the whole mutated object and let the API server diff it, which some admission controllers do, but for a mutating webhook the patch response is the documented mechanism and it keeps the response small and reviewable.
The library also goes beyond comparison. The repository contains apply.go, merge.go, equal.go, differ.go, lcs.go, hash.go, json.go, option.go, pointer.go, operation.go and patch.go, so the package can apply a patch to a document, produce a merge patch, compare two values for equality, and it contains a longest common subsequence implementation used when deciding how to express changes to arrays.
Two dependencies do most of the work: gjson v1.18.0 for reading values out of JSON without unmarshalling them into structs, and sjson v1.2.5 for setting and deleting values at a path. The go.mod declares go 1.24 and retracts v0.5.1 with the note that the checksum mismatch comes from a rewritten git tag, which is worth knowing if you pin an old version.
Installing the package and calling Compare
The install is a single go get, and the readme states a version requirement:
$ go get github.com/wI2L/jsondiff@latestThe readme calls out that Go 1.21 or later is required, due to the use of the hash/maphash package and the any, min and max keyword and builtin forms. The go.mod in the repository declares go 1.24, which is a higher floor than the readme states, so the practical requirement for a new module is the one in go.mod.
The entry point is Compare, which takes the source and target values and returns the patch. It marshals both with encoding/json first, or with a custom marshal function if you provide one, before comparing them, which means you can hand it any Go value rather than pre-decoded JSON:
patch, err := jsondiff.Compare(pod, newPod)
if err != nil {
// handle error
}For the case where you already have bytes on both sides, CompareJSON is the direct form. That matters for the webhook scenario described below, where the source must be the raw request bytes.
The readme's recommended workflow for a Kubernetes use case has three steps. Copy the original value first, with the type's own DeepCopy method where one exists, or with a dedicated deep copy library for other types, because a shallow copy of a Go struct is discouraged:
newPod := pod.DeepCopy()
// or
podBytes, err := json.Marshal(pod)
if err != nil {
// handle error
}Then mutate the copy:
// Update the image of the webserver container.
newPod.Spec.Containers[0].Image = "nginx:1.19.5-alpine"
// Switch storage medium from memory to default.
newPod.Spec.Volumes[0].EmptyDir.Medium = corev1.StorageMediumDefaultAnd compare. The result is a patch relative to the original, ready for the webhook response payload.
The optional fields trap in Kubernetes webhooks
This is the part of the readme that earns the package its reputation, and it is a real failure mode rather than a theoretical one.
The readme's advice is that in a real admission controller you should create the diff from the raw bytes of the AdmissionReview.AdmissionRequest.Object.Raw field, not from a hydrated Go struct. The reason, quoted from a Reddit comment referenced in the readme, is that the hydrated corev1.Pod object may contain optional fields, being ones that are a struct type but are not pointers to those structs. These will exist when you unmarshal from JSON, because of how Go structs work, but are not in the original JSON.
The consequence is concrete. Comparing the unmarshalled version against a copy produces add and change patches below a path that was never in the original JSON, and the Kubernetes API server will reject that patch. So a webhook that looks correct in a local test can fail in production for reasons that have nothing to do with your mutation logic.
The working form looks like this:
podBytes, err := json.Marshal(pod)
if err != nil {
// handle error
}
// req is a k8s.io/api/admission/v1.AdmissionRequest object
jsondiff.CompareJSON(req.AdmissionRequest.Object.Raw, podBytes)The readme also points at a kubebuilder issue with a detailed description of the problem and its resolution, and it notes that whether you mutate the original pod object or a copy is your choice, as long as the raw bytes are the source of the comparison.
There is a second downside listed in the same section, about the webhook not having the latest client-go package, where the readme is truncated in the copy available here. What survives in the text is that an outdated dependency is the other way this example can go wrong, which is a normal consequence of comparing typed structs against request bytes.
Where the position based comparison runs out
The library compares arrays by position, which is the assumption that makes the diff small and the patch applicable, and it is also where the semantics get tricky.
If a source array is [a, b, c] and a target array is [c, a, b], a position based comparison produces a remove of a, a remove of b, an add of c at the front, an add of a and an add of b, or some equivalent sequence. Every element moves, so the patch is large, and the operation ordering has to be such that applying it in sequence yields the target. The presence of lcs.go, a longest common subsequence implementation, indicates that the library tries to find elements that stay in place and only expresses the genuinely reordered ones, which is the standard approach for keeping patches minimal.
Even with that, the output is not a semantic diff of an array. It is a script of operations that happens to transform one array into another. If your use case depends on knowing that an element moved rather than that the array differs, this is the wrong tool.
There is also a semantic question about numbers and types. RFC6902 has no numeric type, and a value that differs only in formatting, or an integer written as a float, can round trip through the comparison differently depending on how it was decoded. The readme does not document the number handling, so that is something to check against your own data rather than assume.
And there is the option set in option.go, which is where behaviours like custom marshalling, the handling of the default rule for unknown paths, or in-place application options live. The readme covers only part of that surface, so the godoc at pkg.go.dev is the place to look for the flags you need beyond the default behaviour.
Merge patch, apply, and the wider package
The repository layout tells you the package does more than diffing. There is a merge.go and a topic list that includes merge-patch and RFC7386, and the package is also linked from a Kubernetes webhook and mutating-webhook context. RFC7386 merge patch is the other patch format in common use, the one Kubernetes uses for strategic merge and the format the API server accepts for some update paths, and a library that can produce both is more useful than one that produces one.
apply.go and patch.go indicate the reverse operation: applying a patch back to a document. That is not a toy feature. If your service receives a patch from somewhere and needs to compute the resulting object, or if you want to test that a patch you generate is actually applicable, having the apply side in the same package as the diff side removes a dependency and keeps the semantics consistent.
pointer.go and equal.go cover the value-level questions: pointer equality and deep equality for Go values, which matters when you decide whether a field genuinely changed before emitting an operation for it.
The project also has a playground hosted at jsondiff-play-wi2l.vercel.app, linked from the readme, which lets you try the diff in a browser without writing Go. That is a reasonable way to build intuition about the operation set before wiring it into a service, and it is also the quickest way to check a specific document pair.
The package is MIT licensed, and the repository is not archived with the last push on 2026-07-20. The recent releases are v0.7.1 from 2026-04-01, v0.7.0 from 2025-05-09 and v0.6.1 from 2024-11-17. The version is still below 1.0, which in Go convention means the API is not frozen, so an upgrade can bring signature changes even though the project is under active use.
Against sending the whole object, and against a hand written comparison
Two comparisons show what the library saves you.
The first is against returning the entire mutated object from a mutating webhook instead of a patch. The difference is in what the API server does with your response. A patch response tells the server exactly which paths changed, so the server can apply the change to the stored object and leave the rest untouched, and the audit trail shows the specific fields. A full object response replaces the object, and any field the API server has defaulted that your copy did not have gets overwritten or rejected. The optional fields problem described above is one expression of that difference. The cost of the patch approach is that you have to construct it correctly, which is the work this package takes on.
The second is against writing your own comparison loop. It is not hard to write a recursive function that walks two decoded documents and emits replace and add operations. What is hard is getting the edges right: array reordering, the difference between a missing key and a null value, the treatment of an empty object versus an absent object, and the JSON Pointer escaping rules for keys containing a slash or a tilde. The JSON Patch specification requires that a path segment escape ~ as ~0 and / as ~1, and a hand written implementation that forgets that produces patches the API server rejects with a confusing error. A library that has been through the kubebuilder issue and the ecosystem gets those details right.
So the deciding question is whether you need RFC6902 output. If you only need to know whether two documents are the same, equal.go covers that and a byte comparison of canonical JSON is simpler. If you need to describe a change as a patch, this library is the shortest correct path, and the webhook scenario it was written for is the clearest case.
Editorial conclusion
Adopt jsondiff if you serve a Kubernetes mutating webhook or otherwise need to describe the change between two JSON documents as RFC6902 operations, since it removes the need to hand roll that comparison. Do not adopt it if you need RFC6902 semantics on arbitrary inputs where array element reordering matters, because the comparison walks objects and arrays by position and position based moves are where patch semantics get subtle. Verify first that your service compares against the raw request bytes rather than a re-marshalled Go struct, since the optional fields problem described in the readme causes the API server to reject patches.
Frequently asked questions
How do I install and use wI2L/jsondiff in Go?
Run go get github.com/wI2L/jsondiff@latest and call jsondiff.Compare(source, target), which returns the patch operations and an error. The library marshals both values with encoding/json before comparing them, and CompareJSON does the same when you already hold bytes.
Why does my Kubernetes mutating webhook patch get rejected by the API server?
The readme points at the optional fields problem: a hydrated Go struct contains fields that were not present in the original JSON, so the diff produces paths the API server does not know about. Compare against the raw bytes of AdmissionRequest.Object.Raw instead of the unmarshalled object.
Which Go version does wI2L/jsondiff require?
The readme states Go 1.21 or later, because of the hash/maphash package and the any, min and max builtins, while the go.mod in the repository declares go 1.24. A new module should follow the go.mod requirement.
What licence is wI2L/jsondiff released under and how often is it released?
The package is MIT licensed, with the licence in the LICENSE file. Recent releases are v0.7.1 on 2026-04-01, v0.7.0 on 2025-05-09 and v0.6.1 on 2024-11-17, the repository is not archived, and the last push was on 2026-07-20.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/wi2l-jsondiff)Was this useful?