Skip to content

Commit abdfc17

Browse files
authored
feat: add jsondiff tool (#81)
* feat:add jsondiff tool * add wi2l implementation and test cases * fix lint and format
1 parent 22645b3 commit abdfc17

File tree

8 files changed

+2337
-0
lines changed

8 files changed

+2337
-0
lines changed

go.mod

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ require (
5454
gopkg.in/yaml.v3 v3.0.1
5555
)
5656

57+
require (
58+
github.com/tidwall/gjson v1.18.0 // indirect
59+
github.com/tidwall/match v1.1.1 // indirect
60+
github.com/tidwall/pretty v1.2.1 // indirect
61+
github.com/tidwall/sjson v1.2.5 // indirect
62+
)
63+
5764
require (
5865
cloud.google.com/go/compute/metadata v0.5.0 // indirect
5966
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
@@ -128,6 +135,7 @@ require (
128135
github.com/subosito/gotenv v1.6.0 // indirect
129136
github.com/tklauser/go-sysconf v0.3.14 // indirect
130137
github.com/tklauser/numcpus v0.9.0 // indirect
138+
github.com/wI2L/jsondiff v0.7.0
131139
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
132140
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
133141
github.com/xeipuuv/gojsonschema v1.2.0 // indirect

go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,13 +338,25 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
338338
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
339339
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
340340
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
341+
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
342+
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
343+
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
344+
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
345+
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
346+
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
347+
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
348+
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
349+
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
350+
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
341351
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
342352
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
343353
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
344354
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
345355
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
346356
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
347357
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
358+
github.com/wI2L/jsondiff v0.7.0 h1:1lH1G37GhBPqCfp/lrs91rf/2j3DktX6qYAKZkLuCQQ=
359+
github.com/wI2L/jsondiff v0.7.0/go.mod h1:KAEIojdQq66oJiHhDyQez2x+sRit0vIzC9KeK0yizxM=
348360
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
349361
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
350362
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=

jsondiff/README.md

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
# JSON Diff
2+
3+
A Go library for calculating differences between JSON documents and reconstructing original JSON from diffs. This implementation treats array changes as complete units rather than element-by-element differences.
4+
5+
## Features
6+
7+
- **JSON Comparison**: Generate detailed diffs between two JSON documents
8+
- **Reconstruction**: Reverse diffs to reconstruct the original JSON
9+
- **Array Handling**: Arrays are compared as complete units for cleaner diffs
10+
- **Type Safety**: Preserves data types during comparison and reconstruction
11+
- **Zero Dependencies**: Uses only Go standard library
12+
13+
## Installation
14+
15+
```bash
16+
go get github.com/raystack/salt/jsondiff
17+
```
18+
19+
## Usage
20+
21+
### Custom JSONDiffer
22+
23+
```go
24+
package main
25+
26+
import (
27+
"encoding/json"
28+
"fmt"
29+
"reflect"
30+
"strings"
31+
"github.com/raystack/salt/jsondiff"
32+
)
33+
34+
func main() {
35+
originalJSON := `{
36+
"name": "John",
37+
"matrix4D": [[[[1,2,3],[4,5,6],[4,5,6]]]],
38+
"old_param": "value",
39+
"fruits": {
40+
"apple": 1,
41+
"pears": 2,
42+
"arr": [1,2,3]
43+
}
44+
}`
45+
46+
currentJSON := `{
47+
"name": "John Doe",
48+
"matrix4D": [[[[1,2,3],[4,5,6,7]]]],
49+
"age": 30,
50+
"fruits": {
51+
"apple": 1,
52+
"beans": 2,
53+
"arr": [1,3,4]
54+
},
55+
"new_object": {
56+
"a":1,
57+
"b":[1,2]
58+
}
59+
}`
60+
61+
// Generate diff using custom differ
62+
differ := jsondiff.NewJSONDiffer()
63+
diffs, err := differ.Compare(originalJSON, currentJSON)
64+
if err != nil {
65+
fmt.Printf("Error generating diff: %v\n", err)
66+
return
67+
}
68+
69+
fmt.Printf("Generated %d diff entries:\n", len(diffs))
70+
diffJSON, err := json.MarshalIndent(diffs, "", " ")
71+
if err != nil {
72+
fmt.Printf("Error formatting diff: %v\n", err)
73+
return
74+
}
75+
fmt.Println(string(diffJSON))
76+
77+
// Test reconstruction
78+
reconstructor := jsondiff.NewJSONReconstructor()
79+
reconstructed, err := reconstructor.ReverseDiff(currentJSON, diffs)
80+
if err != nil {
81+
fmt.Printf("Error reconstructing: %v\n", err)
82+
return
83+
}
84+
85+
// Verify reconstruction accuracy
86+
var originalObj, reconstructedObj interface{}
87+
json.Unmarshal([]byte(originalJSON), &originalObj)
88+
json.Unmarshal([]byte(reconstructed), &reconstructedObj)
89+
90+
if reflect.DeepEqual(originalObj, reconstructedObj) {
91+
fmt.Println("✓ Reconstruction successful")
92+
} else {
93+
fmt.Println("✗ Reconstruction failed")
94+
}
95+
}
96+
```
97+
98+
### wI2L JSONDiffer
99+
100+
```go
101+
package main
102+
103+
import (
104+
"encoding/json"
105+
"fmt"
106+
"reflect"
107+
"strings"
108+
"github.com/raystack/salt/jsondiff"
109+
)
110+
111+
func main() {
112+
originalJSON := `{
113+
"name": "John",
114+
"matrix4D": [[[[1,2,3],[4,5,6],[4,5,6]]]],
115+
"old_param": "value",
116+
"fruits": {
117+
"apple": 1,
118+
"pears": 2,
119+
"arr": [1,2,3]
120+
}
121+
}`
122+
123+
currentJSON := `{
124+
"name": "John Doe",
125+
"matrix4D": [[[[1,2,3],[4,5,6,7]]]],
126+
"age": 30,
127+
"fruits": {
128+
"apple": 1,
129+
"beans": 2,
130+
"arr": [1,3,4]
131+
},
132+
"new_object": {
133+
"a":1,
134+
"b":[1,2]
135+
}
136+
}`
137+
138+
// Generate diff using wI2L differ
139+
differ := jsondiff.NewWI2LDiffer()
140+
diffs, err := differ.Compare(originalJSON, currentJSON)
141+
if err != nil {
142+
fmt.Printf("Error generating diff: %v\n", err)
143+
return
144+
}
145+
146+
fmt.Printf("Generated %d diff entries:\n", len(diffs))
147+
diffJSON, err := json.MarshalIndent(diffs, "", " ")
148+
if err != nil {
149+
fmt.Printf("Error formatting diff: %v\n", err)
150+
return
151+
}
152+
fmt.Println(string(diffJSON))
153+
154+
// Test reconstruction
155+
reconstructor := jsondiff.NewJSONReconstructor()
156+
reconstructed, err := reconstructor.ReverseDiff(currentJSON, diffs)
157+
if err != nil {
158+
fmt.Printf("Error reconstructing: %v\n", err)
159+
return
160+
}
161+
162+
// Verify reconstruction accuracy
163+
var originalObj, reconstructedObj interface{}
164+
json.Unmarshal([]byte(originalJSON), &originalObj)
165+
json.Unmarshal([]byte(reconstructed), &reconstructedObj)
166+
167+
if reflect.DeepEqual(originalObj, reconstructedObj) {
168+
fmt.Println("✓ Reconstruction successful")
169+
} else {
170+
fmt.Println("✗ Reconstruction failed")
171+
}
172+
}
173+
```
174+
175+
## Data Structures
176+
177+
### DiffEntry
178+
179+
Each difference is represented as a `DiffEntry`:
180+
181+
```go
182+
type DiffEntry struct {
183+
FieldName string `json:"field_name"` // Name of the changed field
184+
ChangeType string `json:"change_type"` // "added", "removed", or "modified"
185+
FromValue *string `json:"from_value,omitempty"` // Original value (for removed/modified)
186+
ToValue *string `json:"to_value,omitempty"` // New value (for added/modified)
187+
FullPath string `json:"full_path"` // Full JSON path (e.g., "/fruits/apple")
188+
ValueType string `json:"value_type"` // "string", "number", "boolean", "array", "object", "null"
189+
}
190+
```
191+
192+
### Change Types
193+
194+
- **"added"**: Field was added in the new JSON
195+
- **"removed"**: Field was deleted from the original JSON
196+
- **"modified"**: Field value was changed
197+
198+
## Array Handling
199+
200+
Unlike element-by-element array diffs, this library treats entire arrays as single units. When an array changes, the entire array is marked as "modified" with the complete old and new values.
201+
202+
**Example:**
203+
```json
204+
// Original: [1,2,3]
205+
// Current: [1,3,4]
206+
// Result: One "modified" entry with full array values
207+
```
208+
209+
This approach provides cleaner, more predictable diffs for complex nested structures.
210+
211+
## API Reference
212+
213+
### JSONDiffer
214+
215+
```go
216+
func NewJSONDiffer() *JSONDiffer
217+
func (jd *JSONDiffer) Compare(json1, json2 string) ([]DiffEntry, error)
218+
```
219+
220+
### WI2LDiffer
221+
222+
```go
223+
func NewWI2LDiffer() *WI2LDiffer
224+
func (w *WI2LDiffer) Compare(json1, json2 string) ([]DiffEntry, error)
225+
```
226+
227+
### JSONReconstructor
228+
229+
```go
230+
func NewJSONReconstructor() *JSONReconstructor
231+
func (jr *JSONReconstructor) ReverseDiff(currentJSON string, diffs []DiffEntry) (string, error)
232+
```
233+
234+
## Error Handling
235+
236+
The library returns detailed error messages for:
237+
- Invalid JSON syntax
238+
- Malformed diff entries
239+
- Path resolution issues during reconstruction

0 commit comments

Comments
 (0)