README
¶
jsondiff
A powerful Go library and CLI tool for comparing JSON files with colored diff output, line numbers, context lines, and advanced field filtering capabilities.
Features
- Visual Diff Output: Line-by-line comparison with line numbers from both files
- Multiple Display Modes: Standard unified diff or side-by-side comparison
- Smart Highlighting: Color-coded output with inline change highlighting
- Field Filtering: Include or exclude specific fields from comparison
- Nested Field Support: Filter nested fields using dot notation
- Context Control: Configurable context lines around changes
- JSON Normalization: Optional key sorting before comparison
- Customizable Colors: Full color customization via configuration files
- Ignored Field Marking: Excluded fields shown with
~prefix in blue
Installation
Using Go Install
go install github.com/ravinald/jsondiff/cmd/jsondiff@latest
Build from Source
git clone https://github.com/ravinald/jsondiff.git
cd jsondiff
make build
make install
Using Makefile
# Show all available targets
make help
# Build and test
make all
# Install to GOPATH/bin
make install
# Run tests with coverage
make test-coverage
CLI Usage
Basic Usage
jsondiff file1.json file2.json
Command-Line Options
jsondiff [flags] file1.json file2.json
Flags:
-c, --context int Number of context lines to show (default 3)
-s, --sort Sort JSON keys before comparing
-y, --side-by-side Display side-by-side diff
--include strings Fields to include in comparison (comma-separated)
--exclude strings Fields to exclude from comparison (comma-separated)
--config string Path to configuration file
-1 string Marker for lines from first file (default: filename)
-2 string Marker for lines from second file (default: filename)
-b string Marker for lines in both files (default "B")
-h, --help Help for jsondiff
Field Filtering Examples
Include Specific Fields
Only compare name and email fields:
jsondiff --include name,email user1.json user2.json
Output (with file markers):
user1.json {
user1.json - "email": "alice@example.com",
user1.json - "name": "Alice Smith",
user2.json + "email": "alice.j@example.com",
user2.json + "name": "Alice Johnson",
B ~ "~age": 30,
B ~ "~address": {...}
Exclude Specific Fields
Compare everything except timestamp and metadata:
jsondiff --exclude timestamp,metadata data1.json data2.json
Nested Field Filtering
Include only specific nested fields:
# Include only address.city field
jsondiff --include address.city user1.json user2.json
# Include all fields under user.profile
jsondiff --include user.profile config1.json config2.json
# Exclude sensitive nested data
jsondiff --exclude user.password,user.token auth1.json auth2.json
Combined Filters
Use both include and exclude filters:
# Include user fields but exclude user.internal
jsondiff --include user --exclude user.internal data1.json data2.json
Display Options
Custom Source Markers
By default, jsondiff uses the filenames as markers to show which file each line comes from. You can customize these markers:
# Default behavior - uses filenames as markers
jsondiff config.json intent.json
# Output will show:
# config.json - "removed": "value"
# intent.json + "added": "value"
# B ~ "unchanged": "same"
# Use custom markers instead of filenames
jsondiff -1 API -2 Config -b BOTH data1.json data2.json
# Shorter custom markers
jsondiff -1 A -2 B file1.json file2.json
# Use symbols
jsondiff -1 "←" -2 "→" -b "=" left.json right.json
The markers are automatically right-justified and padded to align properly:
API - "old": "value"
Config + "new": "value"
BOTH ~ "unchanged": "same"
In side-by-side view, the markers are used as headers:
# Default - uses filenames as headers
jsondiff -y config.json intent.json
# Custom headers
jsondiff -1 "API Cache" -2 "Site Config" -y api.json site.json
Side-by-Side Comparison
jsondiff -y file1.json file2.json
Output:
file1.json | file2.json
----------------------------------------|----------------------------------------
{ | {
"name": "Alice" | "name": "Bob"
"age": 30 | "age": 31
} | }
Custom Context Lines
# Show 5 lines of context
jsondiff -c 5 file1.json file2.json
# Show no context (only changes)
jsondiff -c 0 file1.json file2.json
Sort Keys Before Comparison
jsondiff -s file1.json file2.json
Configuration File
Create a custom color configuration:
{
"version": 1,
"colors": {
"add": {
"foreground": {
"line": {
"hex": "#00ff00",
"ansi256": 10,
"ansi": 10
},
"inline": {
"hex": "#008000",
"ansi256": 2,
"ansi": 2
}
},
"background": {}
},
"remove": {
"foreground": {
"line": {
"hex": "#ff0000",
"ansi256": 9,
"ansi": 9
},
"inline": {
"hex": "#800000",
"ansi256": 1,
"ansi": 1
}
},
"background": {}
},
"ignored": {
"foreground": {
"hex": "#0000ff",
"ansi256": 12,
"ansi": 12
},
"background": {}
}
}
}
Use the configuration:
jsondiff --config colors.json file1.json file2.json
Library Usage
Basic Example
package main
import (
"fmt"
"log"
"github.com/ravinald/jsondiff/pkg/jsondiff"
)
func main() {
json1 := []byte(`{"name": "Alice", "age": 30}`)
json2 := []byte(`{"name": "Bob", "age": 31}`)
opts := jsondiff.DiffOptions{
ContextLines: 3,
SortJSON: false,
}
diffs, err := jsondiff.Diff(json1, json2, opts)
if err != nil {
log.Fatal(err)
}
// Enhance with inline changes
diffs = jsondiff.EnhanceDiffsWithInlineChanges(diffs)
// Format and display
formatter := jsondiff.NewFormatter(jsondiff.DefaultStyles())
output := formatter.Format(diffs)
fmt.Print(output)
}
Custom Markers Example
package main
import (
"fmt"
"log"
"github.com/ravinald/jsondiff/pkg/jsondiff"
)
func main() {
json1 := []byte(`{"name": "Alice", "age": 30}`)
json2 := []byte(`{"name": "Bob", "age": 31}`)
opts := jsondiff.DiffOptions{
ContextLines: 3,
}
diffs, err := jsondiff.Diff(json1, json2, opts)
if err != nil {
log.Fatal(err)
}
// Format with custom markers
formatter := jsondiff.NewFormatter(jsondiff.DefaultStyles())
formatter.SetMarkers("API", "Config", "BOTH")
output := formatter.Format(diffs)
fmt.Print(output)
}
Field Filtering Example
package main
import (
"fmt"
"log"
"os"
"github.com/ravinald/jsondiff/pkg/jsondiff"
)
func main() {
// Read JSON files
json1, _ := os.ReadFile("user1.json")
json2, _ := os.ReadFile("user2.json")
// Configure diff with field filtering
opts := jsondiff.DiffOptions{
ContextLines: 3,
SortJSON: true,
IncludeFields: []string{"name", "email", "address.city"},
ExcludeFields: []string{"timestamp", "internal"},
}
// Generate diff
diffs, err := jsondiff.Diff(json1, json2, opts)
if err != nil {
log.Fatal(err)
}
// Format output
formatter := jsondiff.NewFormatter(jsondiff.DefaultStyles())
output := formatter.Format(diffs)
fmt.Print(output)
}
Custom Styling Example
package main
import (
"encoding/json"
"fmt"
"github.com/ravinald/jsondiff/pkg/jsondiff"
)
func main() {
// Create custom color configuration
configJSON := `{
"version": 1,
"colors": {
"add": {
"foreground": {
"line": {"hex": "#00ff00", "ansi256": 10, "ansi": 10}
}
},
"remove": {
"foreground": {
"line": {"hex": "#ff0000", "ansi256": 9, "ansi": 9}
}
},
"ignored": {
"foreground": {"hex": "#0000ff", "ansi256": 12, "ansi": 12}
}
}
}`
var config jsondiff.ColorConfig
json.Unmarshal([]byte(configJSON), &config)
// Create styles from config
styles := jsondiff.StylesFromConfig(&config)
// Use custom styles
formatter := jsondiff.NewFormatter(styles)
// ... rest of diff logic
}
Inline Diff Highlighting
jsondiff automatically detects and highlights inline changes within modified lines. When a JSON field value changes but the key remains the same, the tool will:
- Match removed and added lines by their JSON key (e.g., both have key "name")
- Apply bold formatting to the specific changed portion within the value
- Apply faint formatting to unchanged portions of the value
Note: For inline diff highlighting to be applied, lines must meet a similarity threshold - they must be at least 30% similar in character content and their lengths cannot differ by more than 50%. This prevents unrelated lines from being incorrectly paired for inline comparison.
Example:
- "name": "Alice Smith" # "Smith" will be bold, rest will be faint
+ "name": "Alice Johnson" # "Johnson" will be bold, rest will be faint
Side-by-Side Display Example
package main
import (
"fmt"
"github.com/ravinald/jsondiff/pkg/jsondiff"
)
func main() {
json1 := []byte(`{"name": "Alice", "city": "NYC"}`)
json2 := []byte(`{"name": "Bob", "city": "LA"}`)
opts := jsondiff.DiffOptions{
ContextLines: 3,
}
diffs, _ := jsondiff.Diff(json1, json2, opts)
diffs = jsondiff.EnhanceDiffsWithInlineChanges(diffs)
formatter := jsondiff.NewFormatter(jsondiff.DefaultStyles())
// Use FormatSideBySide for side-by-side output
output := formatter.FormatSideBySide(diffs, "file1.json", "file2.json")
fmt.Print(output)
}
API Reference
Types
// DiffOptions configures the diff behavior
type DiffOptions struct {
ContextLines int // Number of context lines to show
SortJSON bool // Sort JSON keys before comparison
IncludeFields []string // Fields to include (empty = all)
ExcludeFields []string // Fields to exclude
}
// DiffLine represents a single line in the diff
type DiffLine struct {
Type DiffType // Equal, Added, or Removed
LineNum1 int // Line number in first file
LineNum2 int // Line number in second file
Content string // Line content
InlineStart int // Start of inline change
InlineEnd int // End of inline change
IsIgnored bool // Field is excluded from comparison
}
// DiffType indicates the type of difference
type DiffType int
const (
DiffTypeEqual DiffType = iota
DiffTypeAdded
DiffTypeRemoved
)
Functions
// Diff compares two JSON byte arrays
func Diff(json1, json2 []byte, opts DiffOptions) ([]DiffLine, error)
// EnhanceDiffsWithInlineChanges adds inline change markers
func EnhanceDiffsWithInlineChanges(diffs []DiffLine) []DiffLine
// NewFormatter creates a formatter with the given styles
func NewFormatter(styles *Styles) *Formatter
// SetMarkers sets custom markers for file1, file2, and both
func (f *Formatter) SetMarkers(file1Marker, file2Marker, bothMarker string)
// Format generates standard diff output
func (f *Formatter) Format(diffs []DiffLine) string
// FormatSideBySide generates side-by-side diff output
func (f *Formatter) FormatSideBySide(diffs []DiffLine, file1Path, file2Path string) string
// DefaultStyles returns the default color styles
func DefaultStyles() *Styles
// StylesFromConfig creates styles from a configuration
func StylesFromConfig(config *ColorConfig) *Styles
Examples
The examples/ directory contains sample JSON files and configurations:
# Run various examples
make example # Basic diff
make example-sort # With sorting
make example-config # With custom colors
make example-side # Side-by-side display
make example-include # Field inclusion
make example-exclude # Field exclusion
make example-nested # Nested field filtering
Development
Running Tests
# Run all tests
make test
# Run with coverage
make test-coverage
# Run with race detector
make test-race
# Run benchmarks
make bench
Code Quality
# Format code
make fmt
# Run go vet
make vet
# Run linter (requires golangci-lint)
make lint
# Run all checks
make ci
Building
# Build binary
make build
# Clean build artifacts
make clean
# Install to GOPATH/bin
make install
Dependencies
- github.com/charmbracelet/lipgloss - Terminal styling and colors
- github.com/spf13/cobra - CLI framework
- github.com/spf13/viper - Configuration management
- golang.org/x/term - Terminal size detection
License
MIT License - see LICENSE file for details
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Author
Created by @ravinald with modern Go best practices and comprehensive feature set for JSON comparison.