golang基于RFC6902标准的JSON差异比较与补丁生成插件库jsondiff的使用
Golang基于RFC6902标准的JSON差异比较与补丁生成插件库jsondiff的使用
简介
jsondiff是一个Go语言包,用于计算两个JSON文档之间的差异,并生成符合RFC6902(JSON Patch)标准的一系列操作。它特别适合用于创建Kubernetes Mutating Webhook的补丁响应。
安装
首先使用以下命令获取最新版本的库:
$ go get github.com/wI2L/jsondiff@latest
重要提示:需要Go 1.21+版本,因为使用了hash/maphash
包以及any/min/max
关键字/内置函数。
使用示例
Kubernetes动态准入控制器
典型的使用场景是比较表示JSON文档源和目标的两个相同类型的值。一个具体应用是生成Kubernetes动态准入控制器返回的补丁。
示例代码
import corev1 "k8s.io/api/core/v1"
pod := corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "webserver",
Image: "nginx:latest",
VolumeMounts: []corev1.VolumeMount{{
Name: "shared-data",
MountPath: "/usr/share/nginx/html",
}},
}},
Volumes: []corev1.Volume{{
Name: "shared-data",
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{
Medium: corev1.StorageMediumMemory,
},
},
}},
},
}
// 复制原始pod值
newPod := pod.DeepCopy()
// 或者
podBytes, err := json.Marshal(pod)
if err != nil {
// 处理错误
}
// 修改pod spec
newPod.Spec.Containers[0].Image = "nginx:1.19.5-alpine"
newPod.Spec.Volumes[0].EmptyDir.Medium = corev1.StorageMediumDefault
// 生成补丁
import "github.com/wI2L/jsondiff"
patch, err := jsondiff.Compare(pod, newPod)
if err != nil {
// 处理错误
}
b, err := json.MarshalIndent(patch, "", " ")
if err != nil {
// 处理错误
}
os.Stdout.Write(b)
输出结果类似:
[{
"op": "replace",
"path": "/spec/containers/0/image",
"value": "nginx:1.19.5-alpine"
}, {
"op": "remove",
"path": "/spec/volumes/0/emptyDir/medium"
}]
可选字段注意事项
在真实的准入控制器中,应该从AdmissionReview.AdmissionRequest.Object.Raw
字段的原始字节创建差异。由于Go结构体的性质,“水合"的corev1.Pod
对象可能包含"可选字段”,导致生成的补丁包含Kubernetes API服务器不知道的添加/更改值。
更现实的用法如下:
podBytes, err := json.Marshal(pod)
if err != nil {
// 处理错误
}
// req是k8s.io/api/admission/v1.AdmissionRequest对象
jsondiff.CompareJSON(req.AdmissionRequest.Object.Raw, podBytes)
选项
如果需要更多控制差异行为,可以将功能选项列表作为Compare
和CompareJSON
函数的第三个参数传递。
操作因子化
默认情况下,包不生成move
或copy
操作。要启用将值移除和添加因子化为移动和复制操作,应使用Factorize()
功能选项。
示例
原始文档:
{
"a": [1, 2, 3],
"b": {"foo": "bar"}
}
更新后的文档:
{
"a": [1, 2, 3],
"c": [1, 2, 3],
"d": {"foo": "bar"}
}
默认生成的补丁:
[
{"op": "remove", "path": "/b"},
{"op": "add", "path": "/c", "value": [1, 2, 3]},
{"op": "add", "path": "/d", "value": {"foo": "bar"}}
]
启用因子化后的补丁:
[
{"op": "copy", "from": "/a", "path": "/c"},
{"op": "move", "from": "/b", "path": "/d"}
]
操作合理化
默认方法使用递归比较,为每个差异生成一个或多个操作。在某些情况下,用针对父节点的单个替换操作替换表示JSON节点内多个更改的一组操作可能是有益的。
使用Rationalize()
选项可以启用此行为。
示例
原始文档:
{
"a": {"b": {"c": {"1": 1, "2": 2, "3": 3}}}
}
更新后的文档:
{
"a": {"b": {"c": {"x": 1, "y": 2, "z": 3}}}
}
默认生成的补丁:
[
{"op": "remove", "path": "/a/b/c/1"},
{"op": "remove", "path": "/a/b/c/2"},
{"op": "remove", "path": "/a/b/c/3"},
{"op": "add", "path": "/a/b/c/x", "value": 1},
{"op": "add", "path": "/a/b/c/y", "value": 2},
{"op": "add", "path": "/a/b/c/z", "value": 3}
]
启用因子化后的补丁:
[
{"op": "move", "from": "/a/b/c/1", "path": "/a/b/c/x"},
{"op": "move", "from": "/a/b/c/2", "path": "/a/b/c/y"},
{"op": "move", "from": "/a/b/c/3", "path": "/a/b/c/z"}
]
启用合理化后的补丁:
[
{"op": "replace", "path": "/a/b/c", "value": {"x": 1, "y": 2, "z": 3}}
]
可逆补丁
使用Invertible()
功能选项,可以指示差异生成器在每个remove
和replace
操作之前加上test
操作。这样的补丁可以反转以使修补后的文档恢复其原始形式。
示例
原始文档:
{
"a": "1",
"b": "2"
}
更新后的文档:
{
"a": "3",
"c": "4"
}
生成的补丁:
[
{"op": "test", "path": "/a", "value": "1"},
{"op": "replace", "path": "/a", "value": "3"},
{"op": "test", "path": "/b", "value": "2"},
{"op": "remove", "path": "/b"},
{"op": "add", "path": "/c", "value": "4"}
]
等效性
某些数据类型(如数组)可以同时深度不相等和等效。对于这种情况,可以使用Equivalent()
选项指示差异生成器跳过生成操作。
LCS(最长公共子序列)
默认用于比较数组的算法是朴素的,可能会生成大量操作。LCS()
选项指示差异生成器计算源数组和目标数组的最长公共子序列,并使用它生成更简洁、更忠实表示差异的操作列表。
忽略
Ignores()
选项允许从生成的差异中排除一个或多个JSON字段/值。字段必须使用JSON Pointer(RFC6901)字符串语法标识。
示例
原始文档:
{
"A": "bar",
"B": "baz",
"C": "foo"
}
更新后的文档:
{
"A": "rab",
"B": "baz",
"D": "foo"
}
使用Ignores("/A", "/B", "/C")
选项后,生成的补丁为空。
MarshalFunc/UnmarshalFunc
默认情况下,包使用标准库encoding/json
包的json.Marshal
和json.Unmarshal
函数将对象编组/解组为JSON。如果希望使用其他包,可以使用MarshalFunc
和UnmarshalFunc
选项进行配置。
自定义解码器示例
patch, err := jsondiff.CompareJSON(
source,
target,
jsondiff.UnmarshalFunc(func(b []byte, v any) error {
dec := json.NewDecoder(bytes.NewReader(b))
dec.UseNumber()
return dec.Decode(v)
}),
)
基准测试
提供了一些基准测试来比较不同JSON文档大小的性能。可以在testdata/benchs
目录中找到这些基准测试使用的JSON文档。
运行基准测试的命令:
go get github.com/cespare/prettybench
go test -bench=. | prettybench
许可证
代码在MIT许可证下授权。
更多关于golang基于RFC6902标准的JSON差异比较与补丁生成插件库jsondiff的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html
更多关于golang基于RFC6902标准的JSON差异比较与补丁生成插件库jsondiff的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
使用jsondiff库实现基于RFC6902的JSON差异比较与补丁
jsondiff是一个遵循RFC6902标准的Go语言库,用于比较两个JSON文档并生成差异补丁。下面我将详细介绍如何使用这个库。
安装
首先安装jsondiff库:
go get github.com/wI2L/jsondiff
基本用法
1. 比较两个JSON文档
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/wI2L/jsondiff"
)
func main() {
// 原始JSON
original := `{
"name": "Alice",
"age": 30,
"address": {
"city": "New York",
"zip": "10001"
},
"hobbies": ["reading", "swimming"]
}`
// 修改后的JSON
modified := `{
"name": "Bob",
"age": 35,
"address": {
"city": "Boston",
"zip": "10001"
},
"hobbies": ["reading", "hiking"],
"married": true
}`
// 解析JSON
var orig, mod interface{}
if err := json.Unmarshal([]byte(original), &orig); err != nil {
log.Fatal(err)
}
if err := json.Unmarshal([]byte(modified), &mod); err != nil {
log.Fatal(err)
}
// 比较JSON并生成补丁
patch, err := jsondiff.Compare(orig, mod)
if err != nil {
log.Fatal(err)
}
// 输出补丁
fmt.Println("Generated patch:")
for _, op := range patch {
fmt.Printf("%s\n", op)
}
}
输出结果:
Generated patch:
{"op":"replace","path":"/name","value":"Bob"}
{"op":"replace","path":"/age","value":35}
{"op":"replace","path":"/address/city","value":"Boston"}
{"op":"replace","path":"/hobbies/1","value":"hiking"}
{"op":"add","path":"/married","value":true}
2. 应用补丁到原始JSON
func applyPatch() {
// 原始JSON
original := `{"name":"Alice","age":30,"hobbies":["reading","swimming"]}`
// 补丁
patch := `[
{"op":"replace","path":"/name","value":"Bob"},
{"op":"add","path":"/married","value":true},
{"op":"replace","path":"/hobbies/1","value":"hiking"}
]`
var orig interface{}
if err := json.Unmarshal([]byte(original), &orig); err != nil {
log.Fatal(err)
}
var p jsondiff.Patch
if err := json.Unmarshal([]byte(patch), &p); err != nil {
log.Fatal(err)
}
// 应用补丁
modified, err := p.Apply(orig)
if err != nil {
log.Fatal(err)
}
// 输出修改后的JSON
result, _ := json.MarshalIndent(modified, "", " ")
fmt.Println("Modified JSON:")
fmt.Println(string(result))
}
输出结果:
Modified JSON:
{
"name": "Bob",
"age": 30,
"hobbies": [
"reading",
"hiking"
],
"married": true
}
高级功能
1. 自定义比较选项
func compareWithOptions() {
original := `{"name":"Alice","age":30}`
modified := `{"name":"Alice","age":30,"nickname":"Ali"}`
var orig, mod interface{}
json.Unmarshal([]byte(original), &orig)
json.Unmarshal([]byte(modified), &mod)
// 创建比较选项
opts := []jsondiff.Option{
jsondiff.Factorize(), // 因子化补丁
jsondiff.Rationalize(), // 合理化补丁
jsondiff.Invertible(), // 生成可逆补丁
jsondiff.Equivalent(), // 考虑等效值
jsondiff.IgnoreEmptyChanges(), // 忽略空变化
}
// 使用选项比较
patch, _ := jsondiff.Compare(orig, mod, opts...)
for _, op := range patch {
fmt.Println(op)
}
}
2. 生成可读的差异报告
func generateDiffReport() {
original := `{"name":"Alice","age":30,"hobbies":["reading","swimming"]}`
modified := `{"name":"Bob","age":35,"hobbies":["reading","hiking"]}`
var orig, mod interface{}
json.Unmarshal([]byte(original), &orig)
json.Unmarshal([]byte(modified), &mod)
patch, _ := jsondiff.Compare(orig, mod)
fmt.Println("Diff Report:")
for _, op := range patch {
switch op.Type {
case jsondiff.OperationAdd:
fmt.Printf("+ Added %s: %v\n", op.Path, op.Value)
case jsondiff.OperationRemove:
fmt.Printf("- Removed %s\n", op.Path)
case jsondiff.OperationReplace:
fmt.Printf("~ Changed %s from %v to %v\n",
op.Path, op.OldValue, op.Value)
case jsondiff.OperationMove:
fmt.Printf("> Moved %s to %s\n", op.From, op.Path)
case jsondiff.OperationCopy:
fmt.Printf("= Copied %s to %s\n", op.From, op.Path)
}
}
}
注意事项
- jsondiff遵循RFC6902标准,支持所有操作类型:add、remove、replace、move和copy
- 路径使用JSON Pointer表示法(如"/address/city")
- 对于大型JSON文档,比较可能会消耗较多内存
- 应用补丁时会验证操作的有效性,无效操作会返回错误
jsondiff库提供了强大而灵活的功能来处理JSON文档之间的差异,非常适合需要实现数据同步、版本控制或审计日志等功能的场景。