Created
October 29, 2024 06:45
-
-
Save olivere/c64c118124f97b4e4f09bc5e9986ec9d to your computer and use it in GitHub Desktop.
JSON diff
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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