Skip to content

Instantly share code, notes, and snippets.

@olivere
Created October 29, 2024 06:45
Show Gist options
  • Save olivere/c64c118124f97b4e4f09bc5e9986ec9d to your computer and use it in GitHub Desktop.
Save olivere/c64c118124f97b4e4f09bc5e9986ec9d to your computer and use it in GitHub Desktop.
JSON diff
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
)
// structureInfo holds information about JSON structure
type structureInfo struct {
Type string // "object", "array", "string", "number", "boolean", "null"
Keys []string // for objects
Length int // for arrays
Children map[string]*structureInfo
}
// getStructure analyzes JSON data and returns its structure
func getStructure(data interface{}) *structureInfo {
info := &structureInfo{
Children: make(map[string]*structureInfo),
}
switch v := data.(type) {
case map[string]interface{}:
info.Type = "object"
for k := range v {
info.Children[k] = getStructure(v[k])
}
case []interface{}:
info.Type = "array"
info.Length = len(v)
if len(v) > 0 {
info.Children["[element]"] = getStructure(v[0])
}
case string:
info.Type = "string"
case float64:
info.Type = "number"
case bool:
info.Type = "boolean"
case nil:
info.Type = "null"
}
return info
}
// compareStructures compares two JSON structures and returns differences
func compareStructures(path string, s1, s2 *structureInfo) []string {
var diffs []string
if s1.Type != s2.Type {
return []string{fmt.Sprintf("- %s: types differ (%s vs %s)", path, s1.Type, s2.Type)}
}
if s1.Type == "object" {
// Check keys present in s1
for k := range s1.Children {
subPath := path
if subPath == "" {
subPath = k
} else {
subPath = subPath + "." + k
}
if _, exists := s2.Children[k]; !exists {
diffs = append(diffs, fmt.Sprintf("- %s: key only in first file", subPath))
continue
}
diffs = append(diffs, compareStructures(subPath, s1.Children[k], s2.Children[k])...)
}
// Check keys present in s2
for k := range s2.Children {
subPath := path
if subPath == "" {
subPath = k
} else {
subPath = subPath + "." + k
}
if _, exists := s1.Children[k]; !exists {
diffs = append(diffs, fmt.Sprintf("+ %s: key only in second file", subPath))
}
}
}
if s1.Type == "array" {
if s1.Length != s2.Length {
diffs = append(diffs, fmt.Sprintf("! %s: array lengths differ (%d vs %d)", path, s1.Length, s2.Length))
}
if s1.Length > 0 && s2.Length > 0 {
elementPath := path + "[]"
diffs = append(diffs, compareStructures(elementPath, s1.Children["[element]"], s2.Children["[element]"])...)
}
}
return diffs
}
func readJSONFile(filename string) (interface{}, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("reading %s: %v", filename, err)
}
var parsed interface{}
if err := json.Unmarshal(data, &parsed); err != nil {
return nil, fmt.Errorf("parsing %s: %v", filename, err)
}
return parsed, nil
}
func main() {
quietMode := flag.Bool("q", false, "quiet mode: only output differences")
flag.Parse()
if flag.NArg() != 2 {
fmt.Fprintf(os.Stderr, "Usage: jsondiff [-q] file1.json file2.json\n")
os.Exit(1)
}
file1, file2 := flag.Arg(0), flag.Arg(1)
data1, err := readJSONFile(file1)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
data2, err := readJSONFile(file2)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
structure1 := getStructure(data1)
structure2 := getStructure(data2)
differences := compareStructures("", structure1, structure2)
if len(differences) == 0 {
if !*quietMode {
fmt.Println("Files have identical structure")
}
os.Exit(0)
}
for _, diff := range differences {
fmt.Println(diff)
}
os.Exit(1)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment