Version Diffing
The versioning package compares two OpenAPI specs and reports every difference — additions, removals, modifications, and breaking changes. It can also generate human-readable changelogs and migration guides so your API consumers know exactly what changed between releases.
Differ
Differ is the entry point for comparing two OpenAPI specs. Pass it the old and new spec (as parsed OpenAPI documents) and it returns a Diff describing every change.
type Differ struct {
IgnoreDescriptions bool // Skip description-only changes
IgnoreExtensions bool // Skip x- extension changes
}Basic comparison
import "github.com/andrianprasetya/open-swag-go/pkg/versioning"
differ := versioning.Differ{}
diff, err := differ.Compare(oldSpec, newSpec)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Total changes: %d\n", len(diff.Changes))
fmt.Printf("Breaking changes: %d\n", len(diff.BreakingChanges))Filtering noise
Description edits and vendor extension tweaks rarely matter to API consumers. Filter them out to focus on structural changes:
differ := versioning.Differ{
IgnoreDescriptions: true,
IgnoreExtensions: true,
}
diff, _ := differ.Compare(oldSpec, newSpec)Diff
Diff is the result of a comparison. It contains every detected change grouped by type.
type Diff struct {
Changes []Change // All changes (additions, removals, modifications)
BreakingChanges []BreakingChange // Subset of changes that break backwards compatibility
Summary string // Human-readable summary line
}Inspecting changes
diff, _ := differ.Compare(oldSpec, newSpec)
for _, c := range diff.Changes {
fmt.Printf("[%s] %s: %s\n", c.Type, c.Path, c.Description)
}
// [added] /users/{id}/avatar: New endpoint
// [modified] /users: Added query parameter "role"
// [removed] /legacy/users: Endpoint removedBreakingChange
BreakingChange represents a single backwards-incompatible change detected between spec versions.
type BreakingChange struct {
Path string // OpenAPI path affected (e.g. "/users/{id}")
Method string // HTTP method (e.g. "GET")
Type string // Category: "endpoint-removed", "field-removed", "type-changed", etc.
Description string // Human-readable explanation
Severity string // "error" or "warning"
}Checking for breaking changes
diff, _ := differ.Compare(oldSpec, newSpec)
if len(diff.BreakingChanges) > 0 {
fmt.Println("⚠️ Breaking changes detected:")
for _, bc := range diff.BreakingChanges {
fmt.Printf(" [%s] %s %s — %s\n", bc.Severity, bc.Method, bc.Path, bc.Description)
}
}
// ⚠️ Breaking changes detected:
// [error] DELETE /users/{id} — Endpoint removed
// [warning] GET /users — Response field "legacy_id" removedSpec Change Examples
Below are before/after examples showing common spec changes and how the differ reports them.
Removing an endpoint
Before
paths:
/users:
get:
summary: List users
/users/{id}:
get:
summary: Get user
delete:
summary: Delete userAfter
paths:
/users:
get:
summary: List users
/users/{id}:
get:
summary: Get user
# DELETE /users/{id} removedThe differ reports this as a breaking change with type endpoint-removed.
Adding a required field
Before
components:
schemas:
CreateUser:
type: object
required: [name, email]
properties:
name:
type: string
email:
type: stringAfter
components:
schemas:
CreateUser:
type: object
required: [name, email, role]
properties:
name:
type: string
email:
type: string
role:
type: string
enum: [admin, user, viewer]Adding a new required field to a request body is a breaking change — existing clients don't send role.
Changing a response type
Before
paths:
/users/{id}:
get:
responses:
"200":
content:
application/json:
schema:
properties:
id:
type: integerAfter
paths:
/users/{id}:
get:
responses:
"200":
content:
application/json:
schema:
properties:
id:
type: string
format: uuidChanging id from integer to string is a breaking change with type type-changed.
ChangelogGenerator
ChangelogGenerator takes a Diff and produces a human-readable changelog in Markdown format.
type ChangelogGenerator struct {
GroupByTag bool // Group changes under API tags
Format string // "markdown" (default) or "plain"
}Generating a changelog
gen := versioning.ChangelogGenerator{
GroupByTag: true,
Format: "markdown",
}
changelog, err := gen.Generate(diff)
if err != nil {
log.Fatal(err)
}
fmt.Println(changelog)
// ## Breaking Changes
//
// ### Users
// - **DELETE /users/{id}** — Endpoint removed
// - **GET /users** — Response field "legacy_id" removed
//
// ## Additions
//
// ### Users
// - **POST /users/{id}/avatar** — New endpoint for uploading user avatarsMigrationGenerator
MigrationGenerator takes a Diff and produces a step-by-step migration guide that tells API consumers exactly what they need to change in their client code.
type MigrationGenerator struct {
IncludeExamples bool // Include code examples in the guide
Format string // "markdown" (default) or "plain"
}Generating a migration guide
gen := versioning.MigrationGenerator{
IncludeExamples: true,
Format: "markdown",
}
guide, err := gen.Generate(diff)
if err != nil {
log.Fatal(err)
}
fmt.Println(guide)
// ## Migration Guide: v1 → v2
//
// ### 1. Remove calls to DELETE /users/{id}
// This endpoint has been removed. Use the new soft-delete endpoint instead:
// `POST /users/{id}/deactivate`
//
// ### 2. Add "role" field to CreateUser requests
// The "role" field is now required when creating a user.
// Valid values: "admin", "user", "viewer"Full Example
A complete program that loads two spec files, compares them, and outputs a changelog and migration guide:
package main
import (
"fmt"
"log"
"os"
"github.com/andrianprasetya/open-swag-go/pkg/versioning"
)
func main() {
oldSpec, err := os.ReadFile("api-v1.yaml")
if err != nil {
log.Fatal(err)
}
newSpec, err := os.ReadFile("api-v2.yaml")
if err != nil {
log.Fatal(err)
}
differ := versioning.Differ{
IgnoreDescriptions: true,
}
diff, err := differ.Compare(oldSpec, newSpec)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Changes: %d, Breaking: %d\n", len(diff.Changes), len(diff.BreakingChanges))
changelogGen := versioning.ChangelogGenerator{GroupByTag: true}
changelog, _ := changelogGen.Generate(diff)
fmt.Println(changelog)
migrationGen := versioning.MigrationGenerator{IncludeExamples: true}
guide, _ := migrationGen.Generate(diff)
fmt.Println(guide)
}