comparator

package module
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 23, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

README

🔬 comparator

main branch GitHub go.mod Go version Go Reference Go Report Card license Release

comparator is a small, dependency-free Go package for deep comparison and rich diffing of arbitrary Go values — primitives, structs, slices, maps, pointers, and complex nested structures. It answers two questions: are these two values equal? and how exactly do they differ? — with output ready for terminals, web UIs, APIs, and version control.

Only the Go standard library is used. There are no third-party dependencies.

✨ Features

  • 🔁 Deep recursive comparison with cycle detection
  • 🔍 Difference detection — detailed, path-addressed reports of what changed
  • 🎨 Multiple output formats — text (with ANSI color), JSON, Markdown, HTML
  • 📐 Unified diff — Unix diff-style output
  • 🩹 JSON Patch — generate and apply RFC 6902 patch documents
  • 🌳 Visual tree diff — hierarchical representation for viewers
  • 🧠 Custom equality — per-type comparators plus auto-detected Equatable/Comparable interfaces
  • 🔤 Type-safe genericsEqualT[T], DiffT[T], and friends
  • ⏱️ Context-aware — cancel or bound long comparisons with context.Context
  • 📡 Streaming reporter — observe each difference as it is found
  • 🎨 Pluggable formatters — register custom output formats (CSV, JUnit, …)
  • 🧪 Testing helpersAssertEqual / RequireEqual for test suites
  • ⚙️ Configurable behavior — float precision, slice-order insensitivity, field/path/map-key ignoring, depth limits, NaN handling, and more
  • 🚫 Zero third-party dependencies — standard library only
  • 📄 Apache-2.0 licensed

📦 Installation

go get github.com/slashdevops/comparator
🔄 Update

Update to the latest available version:

go get -u github.com/slashdevops/comparator

🧰 Requirements

  • Go 1.26 or newer
  • No external Go modules

🚀 Quick Start

Simple equality check
package main

import (
    "fmt"

    "github.com/slashdevops/comparator"
)

func main() {
    comp := comparator.New()
    if comp.Equal(obj1, obj2) {
        fmt.Println("objects are equal")
    }

    // Or use the package-level convenience function:
    if comparator.Equal(obj1, obj2) {
        fmt.Println("objects are equal")
    }
}
Comparison with options
comp := comparator.NewWithOptions(
    comparator.WithFloatPrecision(1e-6),
    comparator.IgnoreSliceOrder(),
    comparator.IgnoreStructFields("ID", "CreatedAt", "UpdatedAt"),
)

if comp.Equal(user1, user2) {
    fmt.Println("users are equal (ignoring ID and timestamps)")
}
Detailed difference analysis
diffComp := comparator.NewDiffComparer(
    comparator.WithOutputFormat("markdown"),
    comparator.WithMaxDiffs(100),
)

result := diffComp.CompareWithDiff(expected, actual)
if !result.Equal {
    fmt.Printf("found %d differences\n", len(result.Differences))
    fmt.Println(result.Summary)

    for _, diff := range result.Differences {
        fmt.Printf("[%s] %s: %s\n", diff.Severity, diff.Path, diff.Message)
    }
}
JSON Patch (RFC 6902)
patch, err := comparator.GetJSONPatch(oldDoc, newDoc)
if err != nil {
    log.Fatal(err)
}

out, _ := json.MarshalIndent(patch, "", "  ")
fmt.Println(string(out))

📖 Documentation

Full documentation lives in the docs/ folder, and the API reference is on pkg.go.dev.

Guide What it covers
🚀 Getting Started Installation, first comparison, core types.
⚙️ Configuration & Options Every option, its default, and when to use it.
🔍 Diffing DiffResult, Difference, diff modes.
🎨 Output Formats Text/color, JSON, Markdown, HTML, unified, visual.
🩹 JSON Patch Generating and applying RFC 6902 patches.
🧠 Custom Comparators Custom equality + Equatable/Comparable.
🧩 Extensibility Generics, reporters, context, pluggable formatters.
🧪 Testing AssertEqual and friends for test suites.
Performance Cost model, benchmarks, tuning.
FAQ Gotchas, thread-safety, common questions.

🎛️ Configuration Options

Option Default Description
WithFloatPrecision(float64) 1e-9 Tolerance for float equality.
IgnoreSliceOrder() false Treat slices as sets (ignore order).
WithMaxDepth(int) 0 (unlimited) Limit recursion depth.
IgnoreUnexported() false Skip unexported struct fields.
WithEquateEmpty(bool) true Toggle nil/empty-container equivalence (both directions).
EquateNaNs() false Treat NaN values as equal.
IgnoreStructFields(...string) none Skip specific struct fields by name.
WithIgnorePaths(...string) none Skip struct fields at exact canonical paths.
WithIgnorePathPatterns(...string) none Skip struct fields whose path matches a regexp.
WithIgnoreMapKeys(...string) none Skip map entries by key.
WithFieldNaming(FieldNaming) GoFieldNaming Use Go field names or json tag names in paths/pointers.
WithTimeLayout(string) time.RFC3339Nano Layout for rendering time.Time.
WithCustomComparator[T](func(T, T) bool) none Register custom equality for a type.
WithReporter(func(Difference)) none Stream each difference to a callback.
WithDiffMode(DiffMode) DiffModeSimple Diff detail level.
WithMaxDiffs(int) 1000 Cap the number of differences collected.
WithOutputFormat(string) "text" text, json, markdown, html, or a registered format.
WithColorize(bool) false ANSI colors in text output.
WithIncludeEqual(bool) false Include equal values in reports.

Struct fields tagged comparator:"-" are always skipped. See Configuration & Options and Extensibility for the full set.

See Configuration & Options for details.

🧵 Thread Safety

Comparator instances are not thread-safe. Create separate instances for concurrent use, or synchronize access with a mutex. The package-level helpers build a fresh instance per call and are safe to call concurrently.

🧪 Testing

Run the test suite:

go test ./...

Run the runnable documentation examples:

go test -run Example ./...

Run benchmarks:

go test -run '^$' -bench . ./...

Run the same local quality checks used by CI:

go fmt ./...
go vet ./...
go test -race -coverprofile=/tmp/comparator-coverage.txt -covermode=atomic ./...
go build ./...

🗂️ Project Structure

.
|-- .github/                            GitHub Actions, CodeQL, Dependabot, release metadata
|-- .golangci.yaml                      Optional local golangci-lint configuration
|-- docs/                               Extensive usage documentation
|-- AGENTS.md                           AI assistant guidance (symlink to copilot-instructions)
|-- doc.go                              Package documentation rendered by pkg.go.dev
|-- comparator.go                       Core comparison and diffing engine
|-- errors.go                           Sentinel errors
|-- generics.go                         Type-safe generic API (EqualT, DiffT, ...)
|-- context.go                          Context-aware comparison
|-- patch.go                            ApplyJSONPatch (RFC 6902 applier)
|-- formatters.go                       Pluggable output formatter registry
|-- options_ext.go                      Additional functional options
|-- stringer.go                         String() helpers for diff types
|-- assert.go                           Testing helpers (AssertEqual, ...)
|-- *_test.go                           Unit tests and benchmarks
|-- *_examples_test.go                  Executable examples
|-- go.mod                              Module definition with no external requirements
|-- LICENSE                             Apache License 2.0
|-- README.md                           Project overview and usage guide
`-- SECURITY.md                         Vulnerability reporting policy

🔐 Security

Security scanning is handled by GitHub CodeQL. See SECURITY.md for the vulnerability reporting policy.

📄 License

comparator is licensed under the Apache License 2.0.

🤝 Contributing

Issues and pull requests are welcome at github.com/slashdevops/comparator. Please keep changes small, idiomatic, tested, documented, and dependency-free unless there is a clear reason to expand the project scope.

Documentation

Overview

Package comparator provides advanced comparison and diffing capabilities for Go data structures.

This package offers a comprehensive solution for deep equality checking and difference detection across any Go values, including primitives, structs, slices, maps, pointers, and complex nested structures. It supports cycle detection, custom comparators, and multiple output formats.

Key Features

  • Deep recursive comparison with cycle detection
  • Configurable comparison behavior (float precision, slice ordering, field ignoring)
  • Multiple output formats: text, JSON, Markdown, HTML
  • Unified diff format (Unix diff-style)
  • JSON Patch generation (RFC 6902)
  • Visual tree diff representation
  • Custom comparators for specific types
  • Detailed statistics and suggestions

Basic Usage

Simple equality check:

comp := comparator.New()
if comp.Equal(obj1, obj2) {
    fmt.Println("Objects are equal")
}

Or use the convenience function:

if comparator.Equal(obj1, obj2) {
    fmt.Println("Objects are equal")
}

Comparison with Options

Configure comparison behavior using functional options:

comp := comparator.NewWithOptions(
    comparator.WithFloatPrecision(1e-6),
    comparator.IgnoreSliceOrder(),
    comparator.IgnoreStructFields("ID", "CreatedAt", "UpdatedAt"),
)

if comp.Equal(user1, user2) {
    fmt.Println("Users are equal (ignoring ID and timestamps)")
}

Detailed Difference Analysis

Get comprehensive diff information:

diffComp := comparator.NewDiffComparer(
    comparator.WithOutputFormat("markdown"),
    comparator.WithMaxDiffs(100),
)

result := diffComp.CompareWithDiff(expected, actual)
if !result.Equal {
    fmt.Printf("Found %d differences\n", len(result.Differences))
    fmt.Println(result.Summary)

    for _, diff := range result.Differences {
        fmt.Printf("[%s] %s: %s\n", diff.Severity, diff.Path, diff.Message)
        for _, suggestion := range diff.Suggestions {
            fmt.Printf("  → %s\n", suggestion)
        }
    }
}

JSON Patch Generation

Generate RFC 6902 JSON Patch documents:

patch, err := comparator.GetJSONPatch(oldDoc, newDoc)
if err != nil {
    log.Fatal(err)
}

patchJSON, _ := json.MarshalIndent(patch, "", "  ")
fmt.Println(string(patchJSON))
// Output:
// [
//   {"op": "replace", "path": "/name", "value": "John"},
//   {"op": "add", "path": "/email", "value": "john@example.com"}
// ]

Unified Diff Format

Generate Unix diff-style output:

diffComp := comparator.NewDiffComparer()
unifiedDiff, err := diffComp.GetUnifiedDiff(oldConfig, newConfig)
if err != nil {
    log.Fatal(err)
}

fmt.Println(unifiedDiff.Header)
for _, chunk := range unifiedDiff.Chunks {
    fmt.Println(chunk.Context)
    for _, change := range chunk.Changes {
        symbol := " "
        if change.Type == "add" {
            symbol = "+"
        } else if change.Type == "remove" {
            symbol = "-"
        }
        fmt.Printf("%s%s\n", symbol, change.Content)
    }
}

Custom Comparators

Register custom comparison logic for specific types:

type User struct {
    ID   int
    Name string
    Age  int
}

comp := comparator.NewWithOptions(
    comparator.WithCustomComparator(func(a, b User) bool {
        return a.ID == b.ID // Compare users by ID only
    }),
)

if comp.Equal(user1, user2) {
    fmt.Println("Users have the same ID")
}

Configuration Options

Available options for customizing comparison behavior:

  • WithFloatPrecision(float64): Set precision for float comparisons
  • IgnoreSliceOrder(): Treat slices as sets (ignore element order)
  • WithMaxDepth(int): Limit recursion depth
  • IgnoreUnexported(): Skip unexported struct fields
  • EquateEmpty(): Treat nil and empty values as equal
  • EquateNaNs(): Treat all NaN values as equal
  • IgnoreStructFields(...string): Skip specific struct fields by name
  • WithTimeLayout(string): Set time format for display
  • WithCustomComparator[T](func(T, T) bool): Register custom comparator
  • WithDiffMode(DiffMode): Set diff reporting mode
  • WithMaxDiffs(int): Limit number of differences collected
  • WithOutputFormat(string): Set output format (text/json/markdown/html)
  • WithColorize(bool): Enable ANSI color codes in text output
  • WithIncludeEqual(bool): Include equal values in diff reports

Colorized Output

Enable ANSI color-coded terminal output for better readability:

comp := comparator.NewDiffComparer(
    comparator.WithColorize(true),
    comparator.WithOutputFormat("text"),
)

result := comp.CompareWithDiff(config1, config2)
formatted, _ := comp.FormatDiff(result, "text")
fmt.Println(formatted) // Displays with colors in terminal

The colorized output uses ANSI color codes:

  • Cyan: Paths and section headers
  • Green: Expected values and equal fields
  • Red: Actual values and errors
  • Yellow: Warnings
  • Bold: Section headers

Use cases for colorized output:

  • Terminal-based tools and CLI applications
  • Development and debugging sessions
  • Interactive diff viewers
  • CI/CD pipeline outputs

Include Equal Values

Include both differences and matches in comparison reports:

comp := comparator.NewDiffComparer(
    comparator.WithIncludeEqual(true),
)

result := comp.CompareWithDiff(user1, user2)
for _, diff := range result.Differences {
    if diff.Detail.Type == "equal" {
        fmt.Printf("✓ %s: values match\n", diff.Path)
    } else {
        fmt.Printf("✗ %s: %s\n", diff.Path, diff.Message)
    }
}

Equal values are marked with:

  • Detail.Type = "equal"
  • Severity = "info"

Use cases for including equal values:

  • Comprehensive audit trails
  • Configuration validation reports
  • Debugging comparison logic
  • Understanding what hasn't changed between versions

Combined Features

Combine colorization and equal values for complete reports:

comp := comparator.NewDiffComparer(
    comparator.WithColorize(true),
    comparator.WithIncludeEqual(true),
    comparator.WithOutputFormat("text"),
)

result := comp.CompareWithDiff(oldConfig, newConfig)
formatted, _ := comp.FormatDiff(result, "text")
fmt.Println(formatted)

This produces a color-coded report showing both differences and matches, making it easy to see the complete picture of the comparison.

Supported Types

The comparator handles all Go types including:

  • Primitives: bool, int*, uint*, float*, complex*, string
  • Composite: arrays, slices, maps, structs
  • Reference: pointers, interfaces, channels, functions
  • Special: time.Time (uses native Equal method)

Thread Safety

Comparator instances are not thread-safe. Create separate instances for concurrent use, or synchronize access with a mutex.

Performance Considerations

  • For repeated comparisons with the same configuration, reuse comparator instances
  • Use WithMaxDepth to limit recursion for deeply nested structures
  • Use WithMaxDiffs to limit memory usage when many differences exist
  • Use IgnoreStructFields to skip expensive field comparisons
  • For large slices, avoid IgnoreSliceOrder when possible (use sorted slices)

Error Handling

Most methods return errors for future compatibility, but current implementations rarely return errors. Always check error returns for forward compatibility.

Example (ColorizedOutput)

Example_colorizedOutput demonstrates using colored terminal output for better readability of differences.

type APIConfig struct {
	Endpoint string
	Timeout  int
	Retries  int
}

config1 := APIConfig{
	Endpoint: "https://api.example.com",
	Timeout:  30,
	Retries:  3,
}

config2 := APIConfig{
	Endpoint: "https://api.example.com",
	Timeout:  60,
	Retries:  5,
}

// Create a comparator with colorized output enabled
comp := NewDiffComparer(
	WithColorize(true),
	WithOutputFormat("text"),
)

result := comp.CompareWithDiff(config1, config2)
formatted, err := comp.FormatDiff(result, "text")
if err != nil {
	fmt.Printf("Error formatting diff: %v\n", err)
	return
}

// The output will contain ANSI color codes for:
// - Cyan: paths and labels
// - Red: removed/expected values
// - Green: added/actual values
// - Yellow: warnings
fmt.Println(formatted)

// Note: In a terminal, this would display with colors.
// Without colorize, the output would be plain text.
Example (ColorizedWithEqualValues)

Example_colorizedWithEqualValues demonstrates combining both features for a complete, colorized comparison report.

type DatabaseConfig struct {
	Host     string
	Port     int
	Database string
	SSL      bool
}

oldConfig := DatabaseConfig{
	Host:     "localhost",
	Port:     5432,
	Database: "myapp",
	SSL:      false,
}

newConfig := DatabaseConfig{
	Host:     "localhost",
	Port:     5432,
	Database: "myapp_prod", // Changed
	SSL:      true,         // Changed
}

// Combine colorization and including equal values
comp := NewDiffComparer(
	WithColorize(true),
	WithIncludeEqual(true),
	WithOutputFormat("text"),
)

result := comp.CompareWithDiff(oldConfig, newConfig)
formatted, err := comp.FormatDiff(result, "text")
if err != nil {
	fmt.Printf("Error formatting diff: %v\n", err)
	return
}

// Output will show:
// - Equal fields (Host, Port) in green with "info" severity
// - Different fields (Database, SSL) in red with "error" severity
// - All paths highlighted in cyan
fmt.Println(formatted)
Example (DetailedDiff)
// Complex JSON-like structures

// Original config
config1 := exampleConfig{
	Version: "1.0.0",
	Server: exampleServerConfig{
		Host: "localhost",
		Port: 8080,
		SSL:  false,
	},
	Database: exampleDatabaseConfig{
		Host:     "db.local",
		Port:     5432,
		Name:     "appdb",
		Username: "admin",
		Password: "secret",
	},
	Features: map[string]bool{
		"auth":       true,
		"logging":    true,
		"debug":      false,
		"monitoring": true,
	},
	Routes: []exampleRoute{
		{
			Path:    "/api/users",
			Methods: []string{"GET", "POST"},
			Handler: "usersHandler",
			Auth:    true,
		},
		{
			Path:    "/api/products",
			Methods: []string{"GET"},
			Handler: "productsHandler",
			Auth:    false,
		},
	},
	Cache: exampleCacheConfig{
		Type:    "redis",
		TTL:     30 * time.Minute,
		MaxSize: 1024 * 1024 * 100, // 100MB
		Enabled: true,
	},
}

// Modified config
config2 := exampleConfig{
	Version: "1.0.1", // Changed
	Server: exampleServerConfig{
		Host: "localhost",
		Port: 8080,
		SSL:  true, // Changed
	},
	Database: exampleDatabaseConfig{
		Host:     "db.local",
		Port:     5432,
		Name:     "appdb",
		Username: "admin",
		Password: "newsecret", // Changed
	},
	Features: map[string]bool{
		"auth":       true,
		"logging":    true,
		"debug":      true, // Changed
		"monitoring": true,
		"caching":    true, // Added
	},
	Routes: []exampleRoute{
		{
			Path:    "/api/users",
			Methods: []string{"GET", "POST", "PUT"}, // Changed
			Handler: "usersHandler",
			Auth:    true,
		},
		{
			Path:    "/api/products",
			Methods: []string{"GET"},
			Handler: "productsHandler",
			Auth:    true, // Changed
		},
		{
			Path:    "/api/orders", // Added
			Methods: []string{"GET", "POST"},
			Handler: "ordersHandler",
			Auth:    true,
		},
	},
	Cache: exampleCacheConfig{
		Type:    "redis",
		TTL:     30 * time.Minute,
		MaxSize: 1024 * 1024 * 500, // Changed
		Enabled: true,
	},
}

// Get detailed diff
result := CompareWithDiff(config1, config2,
	IgnoreStructFields("Password"), // Ignore password changes
	IgnoreSliceOrder(),             // Ignore slice order in routes
	WithDiffMode(DiffModeFull),
	WithMaxDepth(10),
)

// Print formatted diff
formatted, err := NewDiffComparer().FormatDiff(result, "text")
if err != nil {
	fmt.Printf("Error formatting diff: %v\n", err)
	return
}
fmt.Println(formatted)

// Or get JSON Patch
patch, err := GetJSONPatch(config1, config2)
if err != nil {
	fmt.Printf("Error generating patch: %v\n", err)
	return
}
fmt.Printf("\nJSON Patch operations needed: %d\n", len(patch))

// Or get unified diff
unified, err := NewDiffComparer().GetUnifiedDiff(config1, config2)
if err != nil {
	fmt.Printf("Error generating unified diff: %v\n", err)
	return
}
fmt.Printf("\nUnified diff has %d chunks\n", len(unified.Chunks))
Example (IncludeEqualValues)

Example_includeEqualValues demonstrates including both equal and different values in the comparison report.

type User struct {
	ID       int
	Username string
	Email    string
	Active   bool
}

user1 := User{
	ID:       123,
	Username: "john_doe",
	Email:    "john@example.com",
	Active:   true,
}

user2 := User{
	ID:       123,
	Username: "john_doe",
	Email:    "john.doe@example.com", // Changed
	Active:   true,
}

// Create comparator that includes equal values in the report
comp := NewDiffComparer(
	WithIncludeEqual(true),
	WithOutputFormat("text"),
)

result := comp.CompareWithDiff(user1, user2)

fmt.Printf("Total differences (including equal): %d\\n", len(result.Differences))

// Count equal vs different
equalCount := 0
diffCount := 0
for _, diff := range result.Differences {
	if diff.Detail.Type == "equal" {
		equalCount++
	} else {
		diffCount++
	}
}

fmt.Printf("Equal fields: %d\\n", equalCount)
fmt.Printf("Different fields: %d\\n", diffCount)

// This is useful for:
// - Comprehensive audit trails
// - Understanding what hasn't changed
// - Debugging comparison logic
Example (JsonDiff)
// Compare JSON strings directly
json1 := `{
		"user": {
			"id": 12345,
			"name": "John Doe",
			"email": "john@example.com",
			"preferences": {
				"theme": "dark",
				"language": "en"
			},
			"roles": ["admin", "user"]
		}
	}`

json2 := `{
		"user": {
			"id": 12345,
			"name": "Jane Doe",
			"email": "jane@example.com",
			"preferences": {
				"theme": "light",
				"language": "en"
			},
			"roles": ["user", "admin"],
			"active": true
		}
	}`

var data1, data2 any
if err := json.Unmarshal([]byte(json1), &data1); err != nil {
	fmt.Printf("Error unmarshaling json1: %v\n", err)
	return
}
if err := json.Unmarshal([]byte(json2), &data2); err != nil {
	fmt.Printf("Error unmarshaling json2: %v\n", err)
	return
}

result := CompareWithDiff(data1, data2,
	IgnoreSliceOrder(),
	WithDiffMode(DiffModeFull),
)

// Format as Markdown
md, err := NewDiffComparer().FormatDiff(result, "markdown")
if err != nil {
	fmt.Printf("Error formatting as markdown: %v\n", err)
	return
}
fmt.Println(md)

// Or generate HTML report
html, err := NewDiffComparer().FormatDiff(result, "html")
if err != nil {
	fmt.Printf("Error formatting as HTML: %v\n", err)
	return
}
_ = html
// Save to file: os.WriteFile("diff.html", []byte(html), 0644)
Example (PerformanceWithDiff)
// Test with large structures
type BigStruct struct {
	ID     int
	Data   map[string]any
	Nested []map[string][]int
}

// Generate large test data
n := 1000
big1 := &BigStruct{
	ID:     1,
	Data:   make(map[string]any),
	Nested: make([]map[string][]int, n),
}

big2 := &BigStruct{
	ID:     1,
	Data:   make(map[string]any),
	Nested: make([]map[string][]int, n),
}

for i := range n {
	key := fmt.Sprintf("key_%d", i)
	big1.Data[key] = map[string]any{
		"value": i,
		"array": make([]int, 10),
	}
	big2.Data[key] = map[string]any{
		"value": i,
		"array": make([]int, 10),
	}

	big1.Nested[i] = map[string][]int{
		"a": {i, i * 2, i * 3},
		"b": {i, i * 4, i * 5},
	}
	big2.Nested[i] = map[string][]int{
		"a": {i, i * 2, i * 3},
		"b": {i, i * 4, i * 5},
	}
}

// Make one change
big2.Data["key_500"] = map[string]any{
	"value": 999, // Changed
	"array": make([]int, 10),
}

// Compare with diff limiting
result := CompareWithDiff(big1, big2,
	WithMaxDepth(5),
	WithDiffMode(DiffModeFull),
	WithMaxDiffs(100), // Stop after 100 differences
)

fmt.Printf("Found %d differences (limited to first 100)\n", len(result.Differences))
fmt.Printf("Total nodes examined: %d\n", result.PathStats.TotalNodes)
Example (StringLists)

Example_stringLists demonstrates comparing two lists of strings with various options like order-independent comparison.

// Basic string list comparison
list1 := []string{"apple", "banana", "cherry", "date"}
list2 := []string{"apple", "banana", "cherry", "date"}

comp := New()
if comp.Equal(list1, list2) {
	fmt.Println("Lists are equal")
}

// Lists with different order - strict comparison
list3 := []string{"banana", "apple", "cherry", "date"}
if !comp.Equal(list1, list3) {
	fmt.Println("Lists with different order are not equal (strict mode)")
}

// Lists with different order - order-independent comparison
compIgnoreOrder := NewWithOptions(IgnoreSliceOrder())
if compIgnoreOrder.Equal(list1, list3) {
	fmt.Println("Lists with different order are equal (ignore order mode)")
}

// Lists with differences
list4 := []string{"apple", "banana", "cherry", "elderberry"}
diffComp := New()
diffs, err := diffComp.Diff(list1, list4)
if err != nil {
	fmt.Printf("Error getting diff: %v\n", err)
	return
}

if len(diffs) > 0 {
	fmt.Printf("Found %d differences\n", len(diffs))
	for _, diff := range diffs {
		if diff.Path != "" { // Skip summary messages
			fmt.Printf("  - %s: %s\n", diff.Path, diff.Message)
		}
	}
}

// Empty and nil lists
var nilList []string
emptyList := []string{}
compEquateEmpty := NewWithOptions(EquateEmpty())
if compEquateEmpty.Equal(nilList, emptyList) {
	fmt.Println("Nil and empty lists are equal (with EquateEmpty option)")
}
Output:
Lists are equal
Lists with different order are not equal (strict mode)
Lists with different order are equal (ignore order mode)
Found 2 differences
  - [3]: date != elderberry
Nil and empty lists are equal (with EquateEmpty option)
Example (StringListsDiff)

Example_stringListsDiff demonstrates detailed diff generation for string lists showing what changed.

// Original tags
oldTags := []string{"go", "backend", "api", "rest", "database"}

// Updated tags
newTags := []string{"go", "backend", "api", "graphql", "microservices"}

// Create a diff comparer
comp := NewDiffComparer(WithOutputFormat("markdown"))

// Get detailed differences
result := comp.CompareWithDiff(oldTags, newTags)

fmt.Printf("Equal: %v\n", result.Equal)
fmt.Printf("Summary: %s\n\n", result.Summary)

fmt.Println("Differences:")
for _, diff := range result.Differences {
	fmt.Printf("  Path: %s\n", diff.Path)
	fmt.Printf("  Expected: %v\n", diff.Detail.ExpectedValue)
	fmt.Printf("  Actual: %v\n", diff.Detail.ActualValue)
	fmt.Println()
}

// Get JSON Patch for programmatic updates
patch, err := comp.GetJSONPatch(oldTags, newTags)
if err != nil {
	fmt.Printf("Error generating patch: %v\n", err)
	return
}
patchJSON, err := json.MarshalIndent(patch, "", "  ")
if err != nil {
	fmt.Printf("Error marshaling patch: %v\n", err)
	return
}
fmt.Printf("JSON Patch:\n%s\n", string(patchJSON))

// Output format will show:
// Equal: false
// Summary: Found X differences...
// And detailed path-by-path changes
Example (StringListsWithDuplicates)

Example_stringListsWithDuplicates shows how to compare lists that may contain duplicate values.

list1 := []string{"apple", "banana", "apple", "cherry", "banana"}
list2 := []string{"banana", "apple", "cherry", "apple", "banana"}

// Strict order comparison
comp := New()
fmt.Printf("Equal (strict order): %v\n", comp.Equal(list1, list2))

// Order-independent comparison (duplicates must still match)
compIgnoreOrder := NewWithOptions(IgnoreSliceOrder())
fmt.Printf("Equal (ignore order): %v\n", compIgnoreOrder.Equal(list1, list2))

// Different number of duplicates
list3 := []string{"apple", "banana", "cherry", "apple"}
fmt.Printf("Equal with different duplicates: %v\n", compIgnoreOrder.Equal(list1, list3))
Output:
Equal (strict order): false
Equal (ignore order): true
Equal with different duplicates: false

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrUnknownFormat is returned by FormatDiff when the requested format is
	// not a built-in format ("text", "json", "markdown", "html") and no
	// formatter has been registered for it with RegisterFormatter.
	ErrUnknownFormat = errors.New("comparator: unknown output format")

	// ErrCanceled is returned by the context-aware comparison functions and
	// methods when the provided context is canceled or its deadline is exceeded
	// before the comparison completes. It wraps the context's own error, so
	// errors.Is(err, context.Canceled) and errors.Is(err, context.DeadlineExceeded)
	// also report true.
	ErrCanceled = errors.New("comparator: comparison canceled")

	// ErrInvalidPatch is returned by ApplyJSONPatch when a patch operation is
	// malformed, references an unreachable path, or specifies an unsupported
	// operation.
	ErrInvalidPatch = errors.New("comparator: invalid JSON patch")
)

Sentinel errors returned by the package. Use errors.Is to branch on them.

Functions

func ApplyJSONPatch

func ApplyJSONPatch(doc any, patch []JSONPatchOperation) (any, error)

ApplyJSONPatch applies an RFC 6902 JSON Patch to a document and returns the modified document. It supports the "add", "remove", "replace", "move", "copy", and "test" operations.

The document is first normalized to JSON values (objects become map[string]any and arrays become []any) by round-tripping through encoding/json, so doc must be JSON-serializable. The returned value uses those same JSON types.

Patches produced by GetJSONPatch use Go field names by default; generate them with WithFieldNaming(JSONTagNaming) when you intend to apply them to JSON-shaped documents so the pointers line up with the object keys.

A malformed operation, an unreachable path, or an unsupported op returns an error wrapping ErrInvalidPatch. A failed "test" operation also returns an error wrapping ErrInvalidPatch.

Example

ExampleApplyJSONPatch applies a patch to a JSON-shaped document.

doc := map[string]any{"name": "Jon"}
patch := []JSONPatchOperation{
	{Op: "replace", Path: "/name", Value: "John"},
	{Op: "add", Path: "/email", Value: "john@example.com"},
}

got, err := ApplyJSONPatch(doc, patch)
if err != nil {
	fmt.Println("error:", err)
	return
}

m := got.(map[string]any)
fmt.Println(m["name"], m["email"])
Output:
John john@example.com

func AssertEqual

func AssertEqual(t TestingT, want, got any, opts ...Option) bool

AssertEqual fails the test (via t.Errorf) when want and got are not equal under the given options, printing a readable diff of the differences. It returns true when the values are equal. Use it as a drop-in equality check in tests:

comparator.AssertEqual(t, wantUser, gotUser, comparator.IgnoreStructFields("ID"))

func AssertNotEqual

func AssertNotEqual(t TestingT, want, got any, opts ...Option) bool

AssertNotEqual fails the test when want and got are equal under the given options. It returns true when the values differ.

func DeepEqual

func DeepEqual(a, b any, opts ...Option) bool

DeepEqual is an alias for Equal, emphasizing that the comparison is deep (recursive). It provides the same functionality as Equal.

Example:

if comparator.DeepEqual(struct1, struct2) {
    fmt.Println("Structs are deeply equal")
}
Example

ExampleDeepEqual demonstrates DeepEqual, which behaves like Equal but keeps a name that is familiar to users of reflect.DeepEqual.

type point struct{ X, Y int }

fmt.Println(DeepEqual(point{1, 2}, point{1, 2}))
fmt.Println(DeepEqual(point{1, 2}, point{1, 3}))
Output:
true
false

func DeepEqualT

func DeepEqualT[T any](a, b T, opts ...Option) bool

DeepEqualT is an alias for EqualT, emphasizing the recursive comparison. It mirrors DeepEqual for callers who prefer that name.

func Equal

func Equal(a, b any, opts ...Option) bool

Equal is a convenience function that creates a comparator and checks equality between two values. This is useful for one-off comparisons without explicitly creating a comparator instance.

Example:

if comparator.Equal(user1, user2, comparator.IgnoreStructFields("ID")) {
    fmt.Println("Users are equal (ignoring ID)")
}

For multiple comparisons with the same configuration, create a comparator instance using New() or NewWithOptions() for better performance.

Example

ExampleEqual demonstrates the package-level Equal convenience function for a quick deep comparison without constructing a Comparator.

a := map[string]int{"cpu": 2, "mem": 8}
b := map[string]int{"cpu": 2, "mem": 8}

fmt.Println(Equal(a, b))
fmt.Println(Equal(a, map[string]int{"cpu": 4, "mem": 8}))
Output:
true
false
Example (Options)

ExampleEqual_options shows how options can be passed to the package-level helpers to relax the comparison.

left := []int{3, 1, 2}
right := []int{1, 2, 3}

fmt.Println(Equal(left, right))
fmt.Println(Equal(left, right, IgnoreSliceOrder()))
Output:
false
true

func EqualCtx

func EqualCtx(ctx context.Context, a, b any, opts ...Option) (bool, error)

EqualCtx is a package-level convenience for a one-off context-aware equality check.

Example

ExampleEqualCtx shows a context-aware comparison that is canceled up front.

ctx, cancel := context.WithCancel(context.Background())
cancel()

_, err := EqualCtx(ctx, []int{1, 2, 3}, []int{1, 2, 3})
fmt.Println(err != nil)
Output:
true

func EqualT

func EqualT[T any](a, b T, opts ...Option) bool

EqualT reports whether two values of the same type are deeply equal. It is the type-safe counterpart of Equal: the compiler guarantees a and b share a type.

Example:

if comparator.EqualT(user1, user2, comparator.IgnoreStructFields("ID")) {
    // ...
}
Example

ExampleEqualT demonstrates the type-safe generic equality helper.

type point struct{ X, Y int }

fmt.Println(EqualT(point{1, 2}, point{1, 2}))
fmt.Println(EqualT(point{1, 2}, point{3, 4}))
Output:
true
false

func RegisterFormatter

func RegisterFormatter(name string, fn Formatter) bool

RegisterFormatter registers a custom formatter under the given name so that FormatDiff(result, name) and WithOutputFormat(name) can use it. The name is case-insensitive and must not collide with a built-in format ("text", "json", "markdown", "html"). Registering a nil formatter, an empty name, or a built-in name is a no-op that returns false.

RegisterFormatter is safe for concurrent use. Registering the same name again replaces the previous formatter.

Example

ExampleRegisterFormatter adds a custom output format.

RegisterFormatter("summary-only", func(r *DiffResult) (string, error) {
	if r.Equal {
		return "equal", nil
	}
	return "different", nil
})
defer UnregisterFormatter("summary-only")

comp := NewDiffComparer()
result := comp.CompareWithDiff(1, 2)
out, _ := comp.FormatDiff(result, "summary-only")

fmt.Println(out)
Output:
different

func RequireEqual

func RequireEqual(t TestingT, want, got any, opts ...Option)

RequireEqual behaves like AssertEqual but stops the test immediately (via Fatalf when t supports it) instead of merely marking it failed.

func UnregisterFormatter

func UnregisterFormatter(name string) bool

UnregisterFormatter removes a previously registered custom formatter. It returns true if a formatter was removed.

Types

type Change

type Change struct {
	// Type indicates the change type: "add" (+), "remove" (-), or "context" ( )
	Type string `json:"type"`

	// Content is the line content
	Content string `json:"content"`

	// Line is the line number in the original or modified file
	Line int `json:"line,omitempty"`
}

Change represents a single line change in a unified diff.

type Chunk

type Chunk struct {
	// Context describes the location of this chunk (e.g., "@@ -1,10 +1,12 @@")
	Context string `json:"context"`

	// Changes contains the individual line changes in this chunk
	Changes []Change `json:"changes"`
}

Chunk represents a section of a unified diff, containing a group of related changes along with surrounding context lines.

type Comparable

type Comparable[T any] interface {
	// CompareTo compares this instance with another instance of the same type.
	// Returns -1 if this < other, 0 if this == other, 1 if this > other.
	CompareTo(other T) int
}

Comparable is a generic interface for types that can compare themselves to other instances of the same type. Implementations should return:

  • negative integer if the receiver is less than other
  • zero if the receiver equals other
  • positive integer if the receiver is greater than other

This interface is useful for implementing custom ordering and sorting logic.

type Comparator

type Comparator interface {
	// Equal performs a deep equality check between two values using default configuration.
	// Returns true if the values are deeply equal, false otherwise.
	Equal(a, b any) bool

	// EqualWithConfig performs a deep equality check using a custom configuration.
	// This allows fine-grained control over comparison behavior such as float precision,
	// slice ordering, and field ignoring.
	EqualWithConfig(a, b any, config *Config) bool

	// Diff returns a list of all differences found between two values.
	// Each difference includes the path, type, values, and contextual information.
	// Returns an error if the comparison process encounters an issue.
	Diff(a, b any) ([]Difference, error)
}

Comparator is the main comparison interface that provides deep equality checking and difference detection for any Go values. It supports various data types including primitives, structs, slices, maps, pointers, and complex nested structures.

The comparator performs deep recursive comparison with cycle detection to handle self-referential data structures safely. It can be configured with various options to customize comparison behavior.

Example usage:

comp := comparator.New()
if comp.Equal(obj1, obj2) {
    fmt.Println("Objects are equal")
}

func New

func New() Comparator

New creates a new Comparator with default configuration. The default comparator uses:

  • Float precision: 1e-9
  • Equate empty values: true
  • Equate NaNs: false
  • Max depth: unlimited (0)
  • Time layout: RFC3339Nano

Example:

comp := comparator.New()
if comp.Equal(obj1, obj2) {
    fmt.Println("Objects are equal")
}
Example
comp := New()
fmt.Println(comp.Equal(42, 42))
fmt.Println(comp.Equal("left", "right"))
Output:
true
false

func NewWithOptions

func NewWithOptions(opts ...Option) Comparator

NewWithOptions creates a new Comparator with custom options. Options are applied in the order provided. Later options override earlier ones.

Example:

comp := comparator.NewWithOptions(
    comparator.WithFloatPrecision(1e-6),
    comparator.IgnoreSliceOrder(),
    comparator.IgnoreStructFields("ID", "CreatedAt"),
)

if comp.Equal(user1, user2) {
    fmt.Println("Users are equal (ignoring ID and CreatedAt)")
}

type Config

type Config struct {
	// contains filtered or unexported fields
}

Config holds all configuration options for comparison operations. It controls how values are compared, what differences are reported, and how output is formatted. Use Option functions to create and modify Config instances rather than directly manipulating fields.

The zero value is not safe to use; always create Config instances using defaultConfig() or through New/NewWithOptions functions.

type ContextComparator

type ContextComparator interface {
	// EqualCtx behaves like Comparator.Equal but honors ctx cancellation.
	EqualCtx(ctx context.Context, a, b any) (bool, error)

	// DiffCtx behaves like Comparator.Diff but honors ctx cancellation.
	DiffCtx(ctx context.Context, a, b any) ([]Difference, error)

	// CompareWithDiffCtx behaves like DiffComparer.CompareWithDiff but honors
	// ctx cancellation.
	CompareWithDiffCtx(ctx context.Context, a, b any) (*DiffResult, error)
}

ContextComparator is implemented by the comparators returned from New, NewWithOptions, and NewDiffComparer. Its methods accept a context.Context so long-running comparisons over large structures can be canceled or bounded by a deadline.

A canceled context causes the comparison to stop early and return an error that wraps both ErrCanceled and the context's own error.

type DiffComparer

type DiffComparer interface {
	Comparator

	// CompareWithDiff performs a comprehensive comparison and returns a detailed result
	// containing all differences, statistics, and a summary.
	CompareWithDiff(a, b any) *DiffResult

	// GetUnifiedDiff generates a unified diff format output similar to the Unix diff utility.
	// This is useful for displaying differences in a familiar, line-oriented format.
	GetUnifiedDiff(a, b any) (*UnifiedDiff, error)

	// GetJSONPatch generates a JSON Patch (RFC 6902) document describing the differences.
	// The patch can be applied to transform the first value into the second value.
	GetJSONPatch(a, b any) ([]JSONPatchOperation, error)

	// GetVisualDiff generates a tree-based visual representation of the differences.
	// This is useful for creating graphical diff viewers or hierarchical displays.
	GetVisualDiff(a, b any) (*VisualDiff, error)

	// FormatDiff formats a diff result according to the specified format.
	// Supported formats: "text", "json", "markdown", "html".
	FormatDiff(result *DiffResult, format string) (string, error)
}

DiffComparer extends the Comparator interface with advanced diffing capabilities. It provides multiple output formats for differences including unified diff, JSON Patch, and visual tree representations. This interface is ideal for generating human-readable or machine-processable diff reports.

Example usage:

diffComp := comparator.NewDiffComparer(
    comparator.WithOutputFormat("markdown"),
    comparator.WithMaxDiffs(100),
)
result := diffComp.CompareWithDiff(obj1, obj2)
fmt.Println(result.Summary)

func NewDiffComparer

func NewDiffComparer(opts ...Option) DiffComparer

NewDiffComparer creates a new DiffComparer with custom options. A DiffComparer provides all Comparator functionality plus advanced diff reporting capabilities.

Example:

diffComp := comparator.NewDiffComparer(
    comparator.WithOutputFormat("markdown"),
    comparator.WithMaxDiffs(100),
    comparator.WithDiffMode(comparator.DiffModeUnified),
)

result := diffComp.CompareWithDiff(config1, config2)
if !result.Equal {
    formatted, _ := diffComp.FormatDiff(result, "markdown")
    fmt.Println(formatted)
}
Example
type serviceConfig struct {
	Host string
	Port int
}

left := serviceConfig{Host: "localhost", Port: 8080}
right := serviceConfig{Host: "localhost", Port: 9090}

comp := NewDiffComparer()
result := comp.CompareWithDiff(left, right)

fmt.Println(result.Equal)
fmt.Println(len(result.Differences) > 0)
Output:
false
true

type DiffMode

type DiffMode int

DiffMode specifies the format and detail level for reporting differences. Different modes are suitable for different use cases, from simple boolean checks to detailed patch operations.

const (
	// DiffModeSimple provides basic difference reporting with minimal detail.
	// Suitable for quick equality checks with simple diff messages.
	DiffModeSimple DiffMode = iota

	// DiffModeFull provides comprehensive difference reporting with full context,
	// including nested differences and detailed statistics.
	DiffModeFull

	// DiffModeUnified generates differences in unified diff format,
	// similar to the Unix diff utility output.
	DiffModeUnified

	// DiffModeJSONPatch generates differences as JSON Patch operations (RFC 6902),
	// suitable for programmatic application of changes.
	DiffModeJSONPatch

	// DiffModeVisual generates a tree-based visual representation of differences,
	// suitable for graphical diff viewers.
	DiffModeVisual
)

type DiffResult

type DiffResult struct {
	// Summary is a human-readable summary of the comparison result
	Summary string `json:"summary,omitempty"`

	// Differences contains all differences found (empty if Equal is true)
	Differences []Difference `json:"differences,omitempty"`

	// PathStats provides statistics about the comparison process
	PathStats PathStats `json:"stats"`

	// Equal indicates whether the two values are deeply equal
	Equal bool `json:"equal"`
}

DiffResult holds the complete result of a comparison operation. It includes equality status, all differences found, statistics about the comparison process, and a human-readable summary.

Example:

result := comp.CompareWithDiff(obj1, obj2)
if !result.Equal {
    fmt.Printf("Found %d differences\n", len(result.Differences))
    fmt.Println(result.Summary)
    for _, diff := range result.Differences {
        fmt.Printf("  %s: %s\n", diff.Path, diff.Message)
    }
}

func CompareWithDiff

func CompareWithDiff(a, b any, opts ...Option) *DiffResult

CompareWithDiff is a convenience function that creates a diff comparator and performs a comprehensive comparison. It returns a detailed DiffResult containing all differences, statistics, and a summary.

This is useful for one-off detailed comparisons without explicitly creating a comparator instance.

Example:

result := comparator.CompareWithDiff(
    expectedConfig,
    actualConfig,
    comparator.WithOutputFormat("markdown"),
    comparator.WithMaxDiffs(50),
)

if !result.Equal {
    fmt.Println(result.Summary)
    for _, diff := range result.Differences {
        fmt.Printf("  %s: %s\n", diff.Path, diff.Message)
    }
}

For multiple comparisons, create a DiffComparer instance using NewDiffComparer() for better performance.

func CompareWithDiffCtx

func CompareWithDiffCtx(ctx context.Context, a, b any, opts ...Option) (*DiffResult, error)

CompareWithDiffCtx is a package-level convenience for a one-off context-aware comprehensive comparison.

func CompareWithDiffT

func CompareWithDiffT[T any](a, b T, opts ...Option) *DiffResult

CompareWithDiffT performs a comprehensive comparison between two values of the same type and returns a detailed result. It is the type-safe counterpart of CompareWithDiff.

type Difference

type Difference struct {
	// Path is the location of the difference using dot notation (e.g., "User.Address.City")
	Path string `json:"path"`

	// Message is a human-readable description of the difference
	Message string `json:"message"`

	// Severity classifies the importance: "error", "warning", "info"
	Severity string `json:"severity"`

	// Detail contains granular information about the difference
	Detail DifferenceDetail `json:"detail"`

	// Suggestions provides optional recommendations for resolving the difference
	Suggestions []string `json:"suggestions,omitempty"`

	// Level indicates the depth in the structure where this difference was found (0 = root)
	Level int `json:"level"`
}

Difference represents a single difference found during comparison. It includes the location (path), depth level, description, detailed information, severity classification, and optional suggestions for resolution.

The Path field uses dot notation for nested structures:

  • "User.Name" for struct fields
  • "Users[0]" for slice elements
  • "Config[key]" for map entries

func DiffCtx

func DiffCtx(ctx context.Context, a, b any, opts ...Option) ([]Difference, error)

DiffCtx is a package-level convenience for a one-off context-aware diff.

func DiffT

func DiffT[T any](a, b T, opts ...Option) ([]Difference, error)

DiffT returns all differences between two values of the same type. It is the type-safe counterpart of a Comparator.Diff call.

type DifferenceDetail

type DifferenceDetail struct {
	// Type describes the kind of difference:
	// "value_different", "type_mismatch", "missing", "extra_element",
	// "length_mismatch", "missing_key", "extra_key", "nil_mismatch"
	Type string `json:"type"`

	// ExpectedType is the string representation of the expected value's type
	ExpectedType string `json:"expected_type,omitempty"`

	// ActualType is the string representation of the actual value's type
	ActualType string `json:"actual_type,omitempty"`

	// ExpectedValue is the expected value (first argument to comparison)
	ExpectedValue any `json:"expected_value,omitempty"`

	// ActualValue is the actual value (second argument to comparison)
	ActualValue any `json:"actual_value,omitempty"`

	// Children contains nested differences for complex structures
	Children []Difference `json:"children,omitempty"`
}

DifferenceDetail provides granular, type-specific information about a difference. It includes the type of difference, the expected and actual types and values, and any nested differences for complex structures.

type Equatable

type Equatable[T any] interface {
	// Equals returns true if this instance is considered equal to the other instance.
	Equals(other T) bool
}

Equatable is a generic interface for types that can determine equality with other instances of the same type. This provides a type-safe way to implement custom equality logic.

Example implementation:

type Person struct {
    ID   int
    Name string
}

func (p Person) Equals(other Person) bool {
    return p.ID == other.ID
}

type FieldNaming

type FieldNaming int

FieldNaming selects how struct fields are named in difference paths and in JSON Patch pointers.

const (
	// GoFieldNaming uses the Go struct field name (e.g. "Name"). This is the
	// default and preserves backward compatibility.
	GoFieldNaming FieldNaming = iota

	// JSONTagNaming uses the name from the field's `json` struct tag when
	// present, falling back to the Go field name otherwise. This makes JSON
	// Patch pointers align with the JSON representation of the values.
	JSONTagNaming
)

type Formatter

type Formatter func(result *DiffResult) (string, error)

Formatter renders a DiffResult into a string representation. Register one with RegisterFormatter to teach FormatDiff a new output format (for example CSV, JUnit, or SARIF) without changing the package.

type JSONPatchOperation

type JSONPatchOperation struct {
	// Op is the operation type: "add", "remove", "replace", "move", "copy", or "test"
	Op string `json:"op"`

	// Path is the JSON Pointer (RFC 6901) to the target location
	Path string `json:"path"`

	// Value is the value to add, replace, or test (omitted for remove and move)
	Value any `json:"value,omitempty"`

	// From is the source path for move and copy operations
	From string `json:"from,omitempty"`
}

JSONPatchOperation represents a single operation in a JSON Patch document (RFC 6902). JSON Patch is a format for describing changes to a JSON document. It can be used to apply partial updates, avoiding the need to send the entire document.

Example operations:

  • {"op": "replace", "path": "/name", "value": "John"}
  • {"op": "add", "path": "/tags/-", "value": "new-tag"}
  • {"op": "remove", "path": "/deprecated"}
  • {"op": "move", "from": "/old", "path": "/new"}

func GetJSONPatch

func GetJSONPatch(a, b any, opts ...Option) ([]JSONPatchOperation, error)

GetJSONPatch is a convenience function that creates a diff comparator and generates a JSON Patch (RFC 6902) document describing the differences.

This is useful for one-off patch generation without explicitly creating a comparator instance.

Example:

patch, err := comparator.GetJSONPatch(oldDoc, newDoc)
if err != nil {
    log.Fatal(err)
}

patchJSON, _ := json.MarshalIndent(patch, "", "  ")
fmt.Println(string(patchJSON))
// Output:
// [
//   {"op": "replace", "path": "/name", "value": "John"},
//   {"op": "add", "path": "/email", "value": "john@example.com"}
// ]

For multiple patch generations, create a DiffComparer instance using NewDiffComparer() for better performance.

Example

ExampleGetJSONPatch generates an RFC 6902 JSON Patch describing how to turn the first document into the second.

type profile struct {
	Name string `json:"name"`
}

patch, err := GetJSONPatch(profile{Name: "Jon"}, profile{Name: "John"})
if err != nil {
	fmt.Println("error:", err)
	return
}

fmt.Println(len(patch))
fmt.Println(patch[0].Op)
fmt.Println(patch[0].Path)
Output:
1
replace
/Name

func GetJSONPatchT

func GetJSONPatchT[T any](a, b T, opts ...Option) ([]JSONPatchOperation, error)

GetJSONPatchT generates an RFC 6902 JSON Patch describing how to turn a into b, for values of the same type. It is the type-safe counterpart of GetJSONPatch.

type Option

type Option func(*Config)

Option is a function type that configures a Config instance. Options follow the functional options pattern, allowing flexible and readable configuration of comparators.

Example usage:

comp := comparator.NewWithOptions(
    comparator.WithFloatPrecision(1e-6),
    comparator.IgnoreSliceOrder(),
    comparator.WithMaxDepth(10),
)

func EquateEmpty

func EquateEmpty() Option

EquateEmpty configures the comparator to treat nil and empty values as equal. When enabled:

  • nil slice equals empty slice ([]int(nil) == []int{})
  • nil map equals empty map (map[string]int(nil) == map[string]int{})
  • nil pointer equals pointer to zero value
  • empty string equals empty string

This is enabled by default. Use this option explicitly when creating custom configurations to ensure consistent behavior.

Example

ExampleEquateEmpty treats nil and empty containers as equal.

comp := NewWithOptions(EquateEmpty())

var nilSlice []string
emptySlice := []string{}

fmt.Println(comp.Equal(nilSlice, emptySlice))
Output:
true

func EquateNaNs

func EquateNaNs() Option

EquateNaNs configures the comparator to treat NaN (Not-a-Number) values as equal. By default, NaN != NaN according to IEEE 754 standard. Enable this option to consider all NaN values equal to each other.

This is useful in scientific computing or when NaN represents "missing data" or "undefined" consistently across both values.

Example

ExampleEquateNaNs treats NaN values as equal, which the IEEE-754 standard does not do by default.

comp := NewWithOptions(EquateNaNs())

nan := math.NaN()
fmt.Println(comp.Equal(nan, nan))
Output:
true

func IgnoreSliceOrder

func IgnoreSliceOrder() Option

IgnoreSliceOrder configures the comparator to treat slices as sets, ignoring the order of elements. When enabled, [1, 2, 3] will be considered equal to [3, 1, 2].

This is useful when comparing collections where order doesn't matter, such as sets represented as slices or unordered query results.

Note: For simple types, elements are sorted before comparison. For complex types, a more expensive matching algorithm is used.

Example
comp := NewWithOptions(IgnoreSliceOrder())
left := []string{"api", "worker", "db"}
right := []string{"db", "api", "worker"}

fmt.Println(comp.Equal(left, right))
Output:
true

func IgnoreStructFields

func IgnoreStructFields(fields ...string) Option

IgnoreStructFields configures the comparator to skip specified struct fields by name. This applies to all structs encountered during comparison.

Useful for ignoring fields like timestamps, IDs, or other fields that are expected to differ but shouldn't affect equality.

Example:

comp := comparator.NewWithOptions(
    comparator.IgnoreStructFields("CreatedAt", "UpdatedAt", "ID"),
)
Example

ExampleIgnoreStructFields skips volatile fields such as IDs and timestamps so that two otherwise-identical records compare as equal.

type user struct {
	ID        int
	Name      string
	UpdatedAt string
}

comp := NewWithOptions(IgnoreStructFields("ID", "UpdatedAt"))

u1 := user{ID: 1, Name: "Alice", UpdatedAt: "2026-01-01"}
u2 := user{ID: 2, Name: "Alice", UpdatedAt: "2026-07-23"}

fmt.Println(comp.Equal(u1, u2))
Output:
true

func IgnoreUnexported

func IgnoreUnexported() Option

IgnoreUnexported configures the comparator to skip unexported struct fields. By default, unexported fields are compared if accessible through reflection. Enable this option to only compare exported (public) fields.

This is useful when comparing structs where unexported fields may contain internal state that shouldn't affect equality, or when unexported fields are not accessible.

Example

ExampleIgnoreUnexported ignores unexported struct fields during comparison.

type credential struct {
	Name  string
	token string // unexported
}

comp := NewWithOptions(IgnoreUnexported())

c1 := credential{Name: "svc", token: "aaa"}
c2 := credential{Name: "svc", token: "bbb"}

fmt.Println(comp.Equal(c1, c2))
Output:
true

func WithColorize

func WithColorize(enable bool) Option

WithColorize enables colored output for text format diffs. When enabled, differences are highlighted using ANSI color codes:

  • Green: added values
  • Red: removed values
  • Yellow: warnings
  • Cyan: paths and labels

This option only affects text format output. Other formats (JSON, HTML, Markdown) have their own styling mechanisms.

Example:

comp := comparator.NewDiffComparer(
    comparator.WithColorize(true),
    comparator.WithOutputFormat("text"),
)

func WithCustomComparator

func WithCustomComparator[T any](comparator func(a, b T) bool) Option

WithCustomComparator registers a custom comparison function for a specific type T. The comparator function receives two values of type T and returns true if they should be considered equal.

Custom comparators take precedence over default comparison logic for the specified type. This is useful for types with special equality semantics or when you need to override default behavior.

Example:

type User struct {
    ID   int
    Name string
    Age  int
}

comp := comparator.NewWithOptions(
    comparator.WithCustomComparator(func(a, b User) bool {
        return a.ID == b.ID // Compare users by ID only
    }),
)
Example

ExampleWithCustomComparator registers domain-specific comparison logic for a concrete type.

type account struct {
	ID   int
	Name string
}

// Two accounts are considered equal when they share the same ID.
comp := NewWithOptions(WithCustomComparator(func(a, b account) bool {
	return a.ID == b.ID
}))

a1 := account{ID: 7, Name: "prod"}
a2 := account{ID: 7, Name: "production"}

fmt.Println(comp.Equal(a1, a2))
Output:
true

func WithDiffMode

func WithDiffMode(mode DiffMode) Option

WithDiffMode sets the diff reporting mode. See DiffMode constants for available modes and their descriptions.

The default mode is DiffModeSimple.

Example:

comp := comparator.NewDiffComparer(
    comparator.WithDiffMode(comparator.DiffModeUnified),
)

func WithEquateEmpty

func WithEquateEmpty(enable bool) Option

WithEquateEmpty controls whether nil and empty containers (slices and maps) are treated as equal. Unlike EquateEmpty, which can only enable the behavior, this option can also disable it (the default is enabled).

Example:

// Treat nil and empty as distinct.
comp := comparator.NewWithOptions(comparator.WithEquateEmpty(false))
Example

ExampleWithEquateEmpty shows disabling the default nil/empty equivalence.

var nilSlice []int
emptySlice := []int{}

fmt.Println(NewWithOptions(WithEquateEmpty(true)).Equal(nilSlice, emptySlice))
fmt.Println(NewWithOptions(WithEquateEmpty(false)).Equal(nilSlice, emptySlice))
Output:
true
false

func WithFieldNaming

func WithFieldNaming(naming FieldNaming) Option

WithFieldNaming selects how struct field names appear in difference paths and JSON Patch pointers. The default is GoFieldNaming. Use JSONTagNaming to make paths and patches align with the JSON representation of the values.

Example:

comp := comparator.NewDiffComparer(comparator.WithFieldNaming(comparator.JSONTagNaming))
// A field `Name string json:"name"` produces the pointer "/name".
Example

ExampleWithFieldNaming shows JSON-tag-aware JSON Patch pointers.

type profile struct {
	FullName string `json:"full_name"`
}

patch, _ := GetJSONPatchT(
	profile{FullName: "Jon"},
	profile{FullName: "John"},
	WithFieldNaming(JSONTagNaming),
)

fmt.Println(patch[0].Op, patch[0].Path)
Output:
replace /full_name

func WithFloatPrecision

func WithFloatPrecision(precision float64) Option

WithFloatPrecision sets the precision threshold for floating-point comparisons. Two float values are considered equal if their absolute difference is less than or equal to the specified precision.

The default precision is 1e-9. Use a larger value for less strict comparisons, or a smaller value for more strict comparisons.

Example:

comp := comparator.NewWithOptions(
    comparator.WithFloatPrecision(1e-6),
)
// 1.0000001 and 1.0000002 will be considered equal
Example

ExampleWithFloatPrecision shows how to treat floating-point values that are close enough as equal.

comp := NewWithOptions(WithFloatPrecision(1e-3))

fmt.Println(comp.Equal(0.1+0.2, 0.3))
Output:
true

func WithIgnoreMapKeys

func WithIgnoreMapKeys(keys ...string) Option

WithIgnoreMapKeys skips the named map keys anywhere they appear during comparison. Keys are matched by their default string representation, so non-string keys such as integers are matched by their formatted value.

Example:

comp := comparator.NewWithOptions(comparator.WithIgnoreMapKeys("updatedAt", "etag"))
Example

ExampleWithIgnoreMapKeys ignores volatile map keys.

a := map[string]int{"value": 1, "updatedAt": 100}
b := map[string]int{"value": 1, "updatedAt": 999}

fmt.Println(NewWithOptions(WithIgnoreMapKeys("updatedAt")).Equal(a, b))
Output:
true

func WithIgnorePathPatterns

func WithIgnorePathPatterns(patterns ...string) Option

WithIgnorePathPatterns skips struct fields whose canonical path matches any of the given regular expressions. Invalid patterns are ignored so option construction never panics; use regexp.Compile beforehand if you need to validate patterns.

Example:

// Ignore every field named "password" at any depth.
comp := comparator.NewWithOptions(comparator.WithIgnorePathPatterns(`(^|\.)[Pp]assword$`))

func WithIgnorePaths

func WithIgnorePaths(paths ...string) Option

WithIgnorePaths skips struct fields at the given canonical paths. Paths use dotted field names with bracketed index or key segments kept inline, matching the form reported in Difference.Path, e.g. "User.Address.Zip".

Example:

comp := comparator.NewWithOptions(comparator.WithIgnorePaths("Meta.UpdatedAt", "Meta.Etag"))

func WithIncludeEqual

func WithIncludeEqual(enable bool) Option

WithIncludeEqual configures the comparator to include equal values in the diff output. By default, only differences are reported. When enabled, the diff will also show paths where values are equal, providing a complete picture of the comparison.

This is useful for:

  • Generating comprehensive comparison reports
  • Debugging comparison logic
  • Creating detailed audit trails

Note: Enabling this option may significantly increase output size for large structures.

Example:

comp := comparator.NewDiffComparer(
    comparator.WithIncludeEqual(true),
)

func WithMaxDepth

func WithMaxDepth(depth int) Option

WithMaxDepth sets the maximum recursion depth for comparisons. When the specified depth is reached, the comparator performs a shallow comparison instead of recursing further.

A depth of 0 (default) means unlimited depth. Use this option to prevent stack overflows with deeply nested structures or to improve performance by limiting comparison depth.

Example:

comp := comparator.NewWithOptions(
    comparator.WithMaxDepth(5), // Only compare 5 levels deep
)

func WithMaxDiffs

func WithMaxDiffs(max int) Option

WithMaxDiffs sets the maximum number of differences to collect. Once this limit is reached, no more differences will be recorded. This helps prevent memory issues when comparing large structures with many differences.

The default is 1000 differences. Set to 0 for unlimited (use with caution).

Example:

comp := comparator.NewDiffComparer(
    comparator.WithMaxDiffs(50), // Only collect first 50 differences
)

func WithOutputFormat

func WithOutputFormat(format string) Option

WithOutputFormat sets the output format for formatted diffs. Supported formats:

  • "text": Plain text output (default)
  • "json": JSON format
  • "markdown": Markdown format
  • "html": HTML format

Example:

comp := comparator.NewDiffComparer(
    comparator.WithOutputFormat("markdown"),
)

func WithReporter

func WithReporter(reporter func(Difference)) Option

WithReporter registers a callback invoked for every difference as it is discovered during CompareWithDiff, Diff, and the Get*Diff methods. It is useful for streaming, logging, or integrating with test frameworks without waiting for the full result to be materialized.

The reporter is called in traversal order and must not retain the Difference beyond the callback if the caller mutates shared state.

Example:

comp := comparator.NewDiffComparer(comparator.WithReporter(func(d comparator.Difference) {
    log.Printf("%s: %s", d.Path, d.Message)
}))
Example

ExampleWithReporter streams differences via a callback.

type cfg struct{ A, B int }

count := 0
comp := NewDiffComparer(WithReporter(func(Difference) { count++ }))
comp.CompareWithDiff(cfg{1, 2}, cfg{1, 3})

fmt.Println(count > 0)
Output:
true

func WithTimeLayout

func WithTimeLayout(layout string) Option

WithTimeLayout sets the time format layout for time.Time comparisons. This is currently used for formatting but doesn't affect equality checks (time.Time values use the native Equal method).

The default layout is time.RFC3339Nano.

Example:

comp := comparator.NewWithOptions(
    comparator.WithTimeLayout(time.RFC3339),
)
Example

ExampleWithTimeLayout controls how time.Time values are rendered in diff output, while equality still uses the native time comparison.

comp := NewWithOptions(WithTimeLayout(time.RFC3339))

t1 := time.Date(2026, 7, 23, 10, 0, 0, 0, time.UTC)
t2 := time.Date(2026, 7, 23, 10, 0, 0, 0, time.UTC)

fmt.Println(comp.Equal(t1, t2))
Output:
true

type PathStats

type PathStats struct {
	// TotalNodes is the total number of nodes encountered during traversal
	TotalNodes int `json:"total_nodes"`

	// ComparedNodes is the number of nodes that were actually compared
	ComparedNodes int `json:"compared_nodes"`

	// DifferentNodes is the number of nodes found to be different
	DifferentNodes int `json:"different_nodes"`

	// IgnoredNodes is the number of nodes skipped (e.g., due to field ignore rules)
	IgnoredNodes int `json:"ignored_nodes"`
}

PathStats provides statistical information about the comparison operation. It tracks how many nodes were examined, compared, found different, or ignored.

These statistics are useful for understanding the scope and efficiency of the comparison, especially for large or complex data structures.

type TestingT

type TestingT interface {
	Helper()
	Errorf(format string, args ...any)
}

TestingT is the subset of *testing.T (and *testing.B) used by the assertion helpers. Accepting an interface avoids importing the testing package into production builds and lets callers supply their own compatible type.

type UnifiedDiff

type UnifiedDiff struct {
	// Header contains the diff header (e.g., "--- a\n+++ b\n")
	Header string `json:"header"`

	// Chunks contains the individual diff chunks with context
	Chunks []Chunk `json:"chunks"`
}

UnifiedDiff represents differences in unified diff format, similar to the output of the Unix diff utility. This format is familiar to developers and integrates well with version control systems.

The format uses '+' and '-' prefixes to indicate additions and removals.

func (*UnifiedDiff) String

func (u *UnifiedDiff) String() string

String renders a unified diff in the familiar text form: the header followed by each hunk's context header and its change lines. Change contents already carry their "+", "-", or space prefix.

type VisualDiff

type VisualDiff struct {
	// Root is the root node of the visual diff tree
	Root *VisualNode `json:"root"`
}

VisualDiff represents a tree-based visual representation of differences. This structure is suitable for building graphical diff viewers or hierarchical displays in user interfaces.

Each node in the tree contains information about its path, value, status (same/different/added/removed), and child nodes.

func (*VisualDiff) String

func (v *VisualDiff) String() string

String renders a visual diff tree as an indented, one-node-per-line string. Each node is prefixed by a status symbol: "+" added, "-" removed, "~" different, and a space for unchanged nodes.

type VisualNode

type VisualNode struct {
	// Path is the full path to this node (e.g., "root.users[0].name")
	Path string `json:"path"`

	// Value is the string representation of this node's value
	Value string `json:"value"`

	// Status indicates the state: "same", "different", "added", "removed"
	Status string `json:"status"`

	// Children contains child nodes for nested structures
	Children []*VisualNode `json:"children,omitempty"`
}

VisualNode represents a single node in a visual diff tree.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL