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)

选项

如果需要更多控制差异行为,可以将功能选项列表作为CompareCompareJSON函数的第三个参数传递。

操作因子化

默认情况下,包不生成movecopy操作。要启用将值移除和添加因子化为移动和复制操作,应使用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()功能选项,可以指示差异生成器在每个removereplace操作之前加上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.Marshaljson.Unmarshal函数将对象编组/解组为JSON。如果希望使用其他包,可以使用MarshalFuncUnmarshalFunc选项进行配置。

自定义解码器示例

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

1 回复

更多关于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)
		}
	}
}

注意事项

  1. jsondiff遵循RFC6902标准,支持所有操作类型:add、remove、replace、move和copy
  2. 路径使用JSON Pointer表示法(如"/address/city")
  3. 对于大型JSON文档,比较可能会消耗较多内存
  4. 应用补丁时会验证操作的有效性,无效操作会返回错误

jsondiff库提供了强大而灵活的功能来处理JSON文档之间的差异,非常适合需要实现数据同步、版本控制或审计日志等功能的场景。

回到顶部