Documentation
¶
Overview ¶
Package differ provides OpenAPI specification comparison and breaking change detection.
Overview ¶
The differ package enables comparison of OpenAPI specifications to identify differences, categorize changes, and detect breaking API changes. It supports both OAS 2.0 and OAS 3.x documents.
Usage ¶
The package provides two API styles:
- Package-level convenience functions for simple, one-off operations
- Struct-based API for reusable instances with custom configuration
Diff Modes ¶
The differ supports two operational modes:
- ModeSimple: Reports all semantic differences without categorization
- ModeBreaking: Categorizes changes by severity and identifies breaking changes
Change Categories ¶
Changes are categorized by the part of the specification that changed:
- CategoryEndpoint: Path/endpoint changes
- CategoryOperation: HTTP operation changes
- CategoryParameter: Parameter changes
- CategoryRequestBody: Request body changes
- CategoryResponse: Response changes
- CategorySchema: Schema/definition changes
- CategorySecurity: Security scheme changes
- CategoryServer: Server/host changes
- CategoryInfo: Metadata changes
Severity Levels ¶
In ModeBreaking, changes are assigned severity levels:
- SeverityCritical: Critical breaking changes (removed endpoints, operations)
- SeverityError: Breaking changes (removed required parameters, type changes)
- SeverityWarning: Potentially problematic changes (deprecated operations, new required fields)
- SeverityInfo: Non-breaking changes (additions, relaxed constraints)
Example (Simple Diff) ¶
package main
import (
"fmt"
"log"
"github.com/erraggy/oastools/differ"
)
func main() {
// Simple diff using functional options
result, err := differ.DiffWithOptions(
differ.WithSourceFilePath("api-v1.yaml"),
differ.WithTargetFilePath("api-v2.yaml"),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Found %d changes\n", len(result.Changes))
for _, change := range result.Changes {
fmt.Println(change.String())
}
}
Example (Breaking Change Detection) ¶
package main
import (
"fmt"
"log"
"github.com/erraggy/oastools/differ"
)
func main() {
// Diff with breaking mode using functional options
result, err := differ.DiffWithOptions(
differ.WithSourceFilePath("api-v1.yaml"),
differ.WithTargetFilePath("api-v2.yaml"),
differ.WithMode(differ.ModeBreaking),
differ.WithIncludeInfo(true),
)
if err != nil {
log.Fatal(err)
}
if result.HasBreakingChanges {
fmt.Printf("⚠️ Found %d breaking change(s)!\n", result.BreakingCount)
}
fmt.Printf("Summary: %d breaking, %d warnings, %d info\n",
result.BreakingCount, result.WarningCount, result.InfoCount)
// Print changes grouped by severity
for _, change := range result.Changes {
fmt.Println(change.String())
}
}
Example (Reusable Differ Instance) ¶
package main
import (
"fmt"
"log"
"github.com/erraggy/oastools/differ"
)
func main() {
// Create a reusable differ instance
d := differ.New()
d.Mode = differ.ModeBreaking
d.IncludeInfo = false // Skip informational changes
// Compare multiple spec pairs with same configuration
pairs := []struct{ old, new string }{
{"api-v1.yaml", "api-v2.yaml"},
{"api-v2.yaml", "api-v3.yaml"},
{"api-v3.yaml", "api-v4.yaml"},
}
for _, pair := range pairs {
result, err := d.Diff(pair.old, pair.new)
if err != nil {
log.Printf("Error comparing %s to %s: %v", pair.old, pair.new, err)
continue
}
fmt.Printf("\n%s → %s:\n", pair.old, pair.new)
if result.HasBreakingChanges {
fmt.Printf(" ⚠️ %d breaking changes\n", result.BreakingCount)
} else {
fmt.Println(" ✓ No breaking changes")
}
}
}
Working with Parsed Documents ¶
For efficiency when documents are already parsed, use WithSourceParsed and WithTargetParsed:
package main
import (
"fmt"
"log"
"github.com/erraggy/oastools/differ"
"github.com/erraggy/oastools/parser"
)
func main() {
// Parse documents once
source, err := parser.ParseWithOptions(
parser.WithFilePath("api-v1.yaml"),
parser.WithValidateStructure(true),
)
if err != nil {
log.Fatal(err)
}
target, err := parser.ParseWithOptions(
parser.WithFilePath("api-v2.yaml"),
parser.WithValidateStructure(true),
)
if err != nil {
log.Fatal(err)
}
// Compare parsed documents
result, err := differ.DiffWithOptions(
differ.WithSourceParsed(*source),
differ.WithTargetParsed(*target),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Found %d changes\n", len(result.Changes))
}
Change Analysis ¶
The Change struct provides detailed information about each difference:
for _, change := range result.Changes {
fmt.Printf("Path: %s\n", change.Path)
fmt.Printf("Type: %s\n", change.Type)
fmt.Printf("Category: %s\n", change.Category)
fmt.Printf("Severity: %s\n", change.Severity)
fmt.Printf("Message: %s\n", change.Message)
if change.OldValue != nil {
fmt.Printf("Old value: %v\n", change.OldValue)
}
if change.NewValue != nil {
fmt.Printf("New value: %v\n", change.NewValue)
}
}
Breaking Change Examples ¶
Common breaking changes detected in ModeBreaking:
- Removed endpoints or operations (SeverityCritical)
- Removed required parameters (SeverityCritical)
- Changed parameter types (SeverityError)
- Made optional parameters required (SeverityError)
- Removed enum values (SeverityError)
- Removed success response codes (SeverityError)
- Removed schemas (SeverityError)
- Changed authentication requirements (SeverityError)
Non-Breaking Change Examples ¶
Common non-breaking changes in ModeBreaking:
- Added endpoints or operations (SeverityInfo)
- Added optional parameters (SeverityInfo)
- Made required parameters optional (SeverityInfo)
- Added enum values (SeverityInfo)
- Added response codes (SeverityInfo)
- Documentation updates (SeverityInfo)
Version Compatibility ¶
The differ works with:
- OAS 2.0 (Swagger) documents
- OAS 3.0.x documents
- OAS 3.1.x documents
- OAS 3.2.x documents
- Cross-version comparisons (with limitations)
When comparing documents of different OAS versions (e.g., 2.0 vs 3.0), the diff is limited to common elements present in both versions.
Coverage Details ¶
The differ provides comprehensive comparison of OpenAPI specification elements:
Response Comparison ¶
Response objects are fully compared including:
- Headers: All header properties (description, required, deprecated, type, style, schema)
- Content/MediaTypes: Media type objects and their schemas
- Links: Link objects (operationRef, operationId, description)
- Examples: Example map keys (not deep value comparison)
- Extensions: All x-* fields on Response objects
Header comparison includes:
- Description and deprecation status
- Required flag changes
- Type and style modifications
- Schema changes (delegates to schema comparison)
- Extensions on Header objects
MediaType comparison includes:
- Schema changes (delegates to comprehensive schema comparison)
- Extensions on MediaType objects
Link comparison includes:
- Operation references (operationRef, operationId)
- Description changes
- Extensions on Link objects
Schema Comparison ¶
Schema objects are comprehensively compared including all fields:
Metadata:
- title, description
Type information:
- type, format
Numeric constraints:
- multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum
String constraints:
- maxLength, minLength, pattern
Array constraints:
- maxItems, minItems, uniqueItems
Object constraints:
- maxProperties, minProperties
- required fields (with smart severity: adding required=ERROR, removing=INFO)
OAS-specific fields:
- nullable, readOnly, writeOnly, deprecated
Schema comparison uses smart severity assignment in breaking mode:
- ERROR: Stricter constraints (adding required fields, lowering max values, raising min values)
- WARNING: Changes that might affect consumers (type changes, constraint modifications)
- INFO: Relaxations and non-breaking changes (removing required, raising max, lowering min)
Note: Recursive schema properties (properties, items, allOf, oneOf, anyOf, not) are compared separately to avoid cyclic comparison issues.
Extension (x-*) Field Coverage ¶
The OpenAPI Specification allows custom extension fields (starting with "x-") at many levels of the document. The differ detects and reports changes to extensions at commonly-used locations:
Extensions ARE diffed for these types:
- Document level (OAS2Document, OAS3Document)
- Info object
- Server objects
- PathItem objects
- Operation objects
- Parameter objects
- RequestBody objects
- Response objects
- Header objects (response headers)
- Link objects (response links)
- MediaType objects (content types)
- Schema objects
- SecurityScheme objects
- Tag objects
- Components object
Extensions are NOT currently diffed for these less commonly-used types:
- Contact, License, ExternalDocs (nested within Info)
- ServerVariable (nested within Server)
- Reference objects
- Items (OAS 2.0 array item definitions)
- Example, Encoding (response-related nested objects)
- Discriminator, XML (schema-related nested objects)
- OAuthFlows, OAuthFlow (security-related nested objects)
The rationale for this selective coverage is that extensions are most commonly placed at document, path, operation, parameter, response, and schema levels where they provide cross-cutting metadata. Extensions in deeply nested objects like ServerVariable, Discriminator, or Example are rare in practice.
If your use case requires extension diffing for the uncovered types, please open an issue at https://github.com/erraggy/oastools/issues
All extension changes are reported with CategoryExtension and are assigned SeverityInfo in breaking mode, as specification extensions are non-normative and optional according to the OpenAPI Specification.
Example ¶
Example demonstrates basic diff usage with functional options
package main
import (
"fmt"
"log"
"github.com/erraggy/oastools/differ"
)
func main() {
// Compare two OpenAPI specifications
result, err := differ.DiffWithOptions(
differ.WithSourceFilePath("../testdata/petstore-v1.yaml"),
differ.WithTargetFilePath("../testdata/petstore-v2.yaml"),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Found %d changes\n", len(result.Changes))
fmt.Printf("Source version: %s\n", result.SourceVersion)
fmt.Printf("Target version: %s\n", result.TargetVersion)
}
Example (Breaking) ¶
Example_breaking demonstrates breaking change detection
package main
import (
"fmt"
"log"
"github.com/erraggy/oastools/differ"
)
func main() {
result, err := differ.DiffWithOptions(
differ.WithSourceFilePath("../testdata/petstore-v1.yaml"),
differ.WithTargetFilePath("../testdata/petstore-v2.yaml"),
differ.WithMode(differ.ModeBreaking),
differ.WithIncludeInfo(true),
)
if err != nil {
log.Fatal(err)
}
if result.HasBreakingChanges {
fmt.Printf("⚠️ Found %d breaking change(s)\n", result.BreakingCount)
} else {
fmt.Println("✓ No breaking changes detected")
}
fmt.Printf("Summary: %d breaking, %d warnings, %d info\n",
result.BreakingCount, result.WarningCount, result.InfoCount)
}
Example (BreakingChanges) ¶
Example_breakingChanges demonstrates how to detect breaking changes between two API versions and interpret the results by severity.
package main
import (
"fmt"
"log"
"github.com/erraggy/oastools/differ"
)
func main() {
// Compare two API versions with breaking mode enabled
result, err := differ.DiffWithOptions(
differ.WithSourceFilePath("../testdata/petstore-v1.yaml"),
differ.WithTargetFilePath("../testdata/petstore-v2.yaml"),
differ.WithMode(differ.ModeBreaking),
differ.WithIncludeInfo(true), // Include all change levels
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Comparing %s to %s\n", result.SourceVersion, result.TargetVersion)
fmt.Printf("Total changes: %d\n\n", len(result.Changes))
// Critical changes: API consumers WILL break
// Examples: removed endpoints, required parameters, response schema changes
criticalCount := 0
errorCount := 0
warningCount := 0
infoCount := 0
for _, change := range result.Changes {
// Severity constants from internal/severity package:
// SeverityCritical = 3, SeverityError = 2, SeverityWarning = 1, SeverityInfo = 0
switch change.Severity {
case 3: // Critical
criticalCount++
fmt.Printf("CRITICAL [%s]: %s\n", change.Path, change.Message)
case 2: // Error
errorCount++
case 1: // Warning
warningCount++
case 0: // Info
infoCount++
}
}
// Summary by severity
fmt.Printf("\nSummary:\n")
fmt.Printf("- Critical (API will break): %d\n", criticalCount)
fmt.Printf("- Errors (likely to break): %d\n", errorCount)
fmt.Printf("- Warnings (may affect clients): %d\n", warningCount)
fmt.Printf("- Info (non-breaking changes): %d\n", infoCount)
// Check if changes are backward compatible
if result.HasBreakingChanges {
fmt.Println("\n⚠️ This update contains BREAKING CHANGES")
fmt.Println("Consider versioning the API (e.g., /v2/...)")
} else {
fmt.Println("\n✓ Changes are backward compatible")
}
}
Example (ChangeAnalysis) ¶
Example_changeAnalysis demonstrates detailed change analysis
package main
import (
"fmt"
"log"
"github.com/erraggy/oastools/differ"
)
func main() {
d := differ.New()
d.Mode = differ.ModeBreaking
result, err := d.Diff("../testdata/petstore-v1.yaml", "../testdata/petstore-v2.yaml")
if err != nil {
log.Fatal(err)
}
// Group changes by category
categories := make(map[differ.ChangeCategory]int)
for _, change := range result.Changes {
categories[change.Category]++
}
fmt.Println("Changes by category:")
for category, count := range categories {
fmt.Printf(" %s: %d\n", category, count)
}
}
Example (FilterBySeverity) ¶
Example_filterBySeverity demonstrates filtering changes by severity
package main
import (
"fmt"
"log"
"github.com/erraggy/oastools/differ"
)
func main() {
d := differ.New()
d.Mode = differ.ModeBreaking
d.IncludeInfo = false // Exclude info-level changes
result, err := d.Diff("../testdata/petstore-v1.yaml", "../testdata/petstore-v2.yaml")
if err != nil {
log.Fatal(err)
}
// Only breaking changes and warnings remain
fmt.Printf("Breaking and warnings only: %d changes\n", len(result.Changes))
fmt.Printf("Breaking: %d, Warnings: %d\n", result.BreakingCount, result.WarningCount)
}
Example (Parsed) ¶
Example_parsed demonstrates comparing already-parsed documents
package main
import (
"fmt"
"log"
"github.com/erraggy/oastools/differ"
"github.com/erraggy/oastools/parser"
)
func main() {
// Parse documents once
source, err := parser.ParseWithOptions(
parser.WithFilePath("../testdata/petstore-v1.yaml"),
parser.WithValidateStructure(true),
)
if err != nil {
log.Fatal(err)
}
target, err := parser.ParseWithOptions(
parser.WithFilePath("../testdata/petstore-v2.yaml"),
parser.WithValidateStructure(true),
)
if err != nil {
log.Fatal(err)
}
// Compare parsed documents
result, err := differ.DiffWithOptions(
differ.WithSourceParsed(*source),
differ.WithTargetParsed(*target),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Found %d changes between %s and %s\n",
len(result.Changes), result.SourceVersion, result.TargetVersion)
}
Example (ReusableDiffer) ¶
Example_reusableDiffer demonstrates creating a reusable differ instance
package main
import (
"fmt"
"log"
"github.com/erraggy/oastools/differ"
)
func main() {
// Create a reusable differ with specific configuration
d := differ.New()
d.Mode = differ.ModeBreaking
d.IncludeInfo = false
d.UserAgent = "my-api-tool/1.0"
// Use the same differ for multiple comparisons
specs := []struct{ old, new string }{
{"../testdata/petstore-v1.yaml", "../testdata/petstore-v2.yaml"},
}
for _, spec := range specs {
result, err := d.Diff(spec.old, spec.new)
if err != nil {
log.Printf("Error: %v", err)
continue
}
fmt.Printf("%s → %s: ", spec.old, spec.new)
if result.HasBreakingChanges {
fmt.Printf("%d breaking\n", result.BreakingCount)
} else {
fmt.Println("compatible")
}
}
}
Example (Simple) ¶
Example_simple demonstrates simple diff mode
package main
import (
"fmt"
"log"
"github.com/erraggy/oastools/differ"
)
func main() {
result, err := differ.DiffWithOptions(
differ.WithSourceFilePath("../testdata/petstore-v1.yaml"),
differ.WithTargetFilePath("../testdata/petstore-v2.yaml"),
differ.WithMode(differ.ModeSimple),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Simple diff found %d changes\n", len(result.Changes))
// Print first few changes
for i, change := range result.Changes {
if i >= 3 {
break
}
fmt.Println(change.String())
}
}
Index ¶
- Constants
- type Change
- type ChangeCategory
- type ChangeType
- type DiffMode
- type DiffResult
- type Differ
- type Option
- func WithIncludeInfo(enabled bool) Option
- func WithMode(mode DiffMode) Option
- func WithSourceFilePath(path string) Option
- func WithSourceParsed(result parser.ParseResult) Option
- func WithTargetFilePath(path string) Option
- func WithTargetParsed(result parser.ParseResult) Option
- func WithUserAgent(ua string) Option
- type Severity
Examples ¶
Constants ¶
const ( // SeverityInfo indicates informational changes (additions, relaxed constraints) SeverityInfo = severity.SeverityInfo // SeverityWarning indicates potentially problematic changes SeverityWarning = severity.SeverityWarning // SeverityError indicates breaking changes (removed features, stricter constraints) SeverityError = severity.SeverityError // SeverityCritical indicates critical breaking changes (removed endpoints, operations) SeverityCritical = severity.SeverityCritical )
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Change ¶
type Change struct {
// Path is the JSON path to the changed element (e.g., "paths./pets.get")
Path string
// Type indicates if this is an addition, removal, or modification
Type ChangeType
// Category indicates which part of the spec was changed
Category ChangeCategory
// Severity indicates the impact level (only used in ModeBreaking)
Severity Severity
// OldValue is the value in the source document (nil for additions)
OldValue any
// NewValue is the value in the target document (nil for removals)
NewValue any
// Message is a human-readable description of the change
Message string
}
Change represents a single difference between two OpenAPI specifications
type ChangeCategory ¶
type ChangeCategory string
ChangeCategory indicates which part of the spec was changed
const ( // CategoryEndpoint indicates a path/endpoint change CategoryEndpoint ChangeCategory = "endpoint" // CategoryOperation indicates an HTTP operation change CategoryOperation ChangeCategory = "operation" // CategoryParameter indicates a parameter change CategoryParameter ChangeCategory = "parameter" // CategoryRequestBody indicates a request body change CategoryRequestBody ChangeCategory = "request_body" // CategoryResponse indicates a response change CategoryResponse ChangeCategory = "response" // CategorySchema indicates a schema/definition change CategorySchema ChangeCategory = "schema" // CategorySecurity indicates a security scheme change CategorySecurity ChangeCategory = "security" // CategoryServer indicates a server change CategoryServer ChangeCategory = "server" // CategoryInfo indicates metadata change (info, contact, license, etc.) CategoryInfo ChangeCategory = "info" // CategoryExtension indicates a specification extension (x-*) change CategoryExtension ChangeCategory = "extension" )
type ChangeType ¶
type ChangeType string
ChangeType indicates whether a change is an addition, removal, or modification
const ( // ChangeTypeAdded indicates a new element was added ChangeTypeAdded ChangeType = "added" // ChangeTypeRemoved indicates an element was removed ChangeTypeRemoved ChangeType = "removed" // ChangeTypeModified indicates an existing element was changed ChangeTypeModified ChangeType = "modified" )
type DiffResult ¶
type DiffResult struct {
// SourceVersion is the source document's OAS version string
SourceVersion string
// SourceOASVersion is the enumerated source OAS version
SourceOASVersion parser.OASVersion
// TargetVersion is the target document's OAS version string
TargetVersion string
// TargetOASVersion is the enumerated target OAS version
TargetOASVersion parser.OASVersion
// Changes contains all detected changes
Changes []Change
// BreakingCount is the number of breaking changes (Critical + Error severity)
BreakingCount int
// WarningCount is the number of warnings
WarningCount int
// InfoCount is the number of informational changes
InfoCount int
// HasBreakingChanges is true if any breaking changes were detected
HasBreakingChanges bool
}
DiffResult contains the results of comparing two OpenAPI specifications
func Diff
deprecated
func Diff(sourcePath, targetPath string) (*DiffResult, error)
Diff is a convenience function that compares two OpenAPI specification files. It's equivalent to creating a Differ with New() and calling Diff().
For one-off diff operations, this function provides a simpler API. For comparing multiple files with the same configuration, create a Differ instance and reuse it.
Deprecated: Use DiffWithOptions for a more flexible API.
Example:
result, err := differ.Diff("api-v1.yaml", "api-v2.yaml")
if err != nil {
log.Fatal(err)
}
if result.HasBreakingChanges {
// Handle breaking changes
}
func DiffParsed
deprecated
func DiffParsed(source, target parser.ParseResult) (*DiffResult, error)
DiffParsed is a convenience function that compares two already-parsed OpenAPI specifications.
Deprecated: Use DiffWithOptions for a more flexible API.
Example:
source, _ := parser.Parse("api-v1.yaml", false, true)
target, _ := parser.Parse("api-v2.yaml", false, true)
result, err := differ.DiffParsed(*source, *target)
func DiffWithOptions ¶ added in v1.11.0
func DiffWithOptions(opts ...Option) (*DiffResult, error)
DiffWithOptions compares two OpenAPI specifications using functional options. This provides a flexible, extensible API that combines input source selection and configuration in a single function call.
Example:
result, err := differ.DiffWithOptions(
differ.WithSourceFilePath("api-v1.yaml"),
differ.WithTargetFilePath("api-v2.yaml"),
differ.WithMode(differ.ModeBreaking),
)
type Differ ¶
type Differ struct {
// Mode determines the type of diff operation (Simple or Breaking)
Mode DiffMode
// IncludeInfo determines whether to include informational changes
IncludeInfo bool
// UserAgent is the User-Agent string used when fetching URLs
// Defaults to "oastools" if not set
UserAgent string
}
Differ handles OpenAPI specification comparison
func (*Differ) Diff ¶
func (d *Differ) Diff(sourcePath, targetPath string) (*DiffResult, error)
Diff compares two OpenAPI specification files
func (*Differ) DiffParsed ¶
func (d *Differ) DiffParsed(source, target parser.ParseResult) (*DiffResult, error)
DiffParsed compares two already-parsed OpenAPI specifications
type Option ¶ added in v1.11.0
type Option func(*diffConfig) error
Option is a function that configures a diff operation
func WithIncludeInfo ¶ added in v1.11.0
WithIncludeInfo enables or disables informational changes Default: true
func WithMode ¶ added in v1.11.0
WithMode sets the diff mode (Simple or Breaking) Default: ModeSimple
func WithSourceFilePath ¶ added in v1.11.0
WithSourceFilePath specifies a file path or URL as the source document
func WithSourceParsed ¶ added in v1.11.0
func WithSourceParsed(result parser.ParseResult) Option
WithSourceParsed specifies a parsed ParseResult as the source document
func WithTargetFilePath ¶ added in v1.11.0
WithTargetFilePath specifies a file path or URL as the target document
func WithTargetParsed ¶ added in v1.11.0
func WithTargetParsed(result parser.ParseResult) Option
WithTargetParsed specifies a parsed ParseResult as the target document
func WithUserAgent ¶ added in v1.11.0
WithUserAgent sets the User-Agent string for HTTP requests Default: "" (uses parser default)