Version Diffing Example
This example shows how to use the versioning package to compare two OpenAPI specs, detect breaking changes, and generate a changelog and migration guide. This is useful in CI pipelines to catch backwards-incompatible changes before they ship.
The Full Program
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
openswag "github.com/gopackx/open-swag-go"
"github.com/gopackx/open-swag-go/pkg/versioning"
)
func main() {
// --- v1 spec ---
v1Config := openswag.Config{
Info: openswag.Info{
Title: "Task API",
Version: "1.0.0",
},
}
v1Endpoints := []openswag.Endpoint{
{
Method: "GET",
Path: "/tasks",
Summary: "List tasks",
Tags: []string{"Tasks"},
Responses: openswag.Responses{
200: {Description: "Task list"},
},
},
{
Method: "POST",
Path: "/tasks",
Summary: "Create a task",
Tags: []string{"Tasks"},
RequestBody: &openswag.RequestBody{
Description: "Task to create",
ContentType: "application/json",
Required: true,
},
Responses: openswag.Responses{
201: {Description: "Task created"},
},
},
{
Method: "DELETE",
Path: "/tasks/{id}",
Summary: "Delete a task",
Tags: []string{"Tasks"},
Parameters: []openswag.Parameter{
{Name: "id", In: "path", Required: true, Description: "Task ID"},
},
Responses: openswag.Responses{
204: {Description: "Task deleted"},
},
},
}
v1Docs := openswag.New(v1Config)
v1Docs.AddAll(v1Endpoints...)
// --- v2 spec ---
v2Config := openswag.Config{
Info: openswag.Info{
Title: "Task API",
Version: "2.0.0",
},
}
v2Endpoints := []openswag.Endpoint{
{
Method: "GET",
Path: "/tasks",
Summary: "List tasks",
Tags: []string{"Tasks"},
// v2 adds pagination parameters
Parameters: []openswag.Parameter{
{Name: "page", In: "query", Description: "Page number"},
{Name: "limit", In: "query", Description: "Items per page"},
},
Responses: openswag.Responses{
200: {Description: "Paginated task list"},
},
},
{
Method: "POST",
Path: "/tasks",
Summary: "Create a task",
Tags: []string{"Tasks"},
// v2 adds auth requirement (breaking change)
Security: []string{openswag.SecurityBearerAuth},
RequestBody: &openswag.RequestBody{
Description: "Task to create",
ContentType: "application/json",
Required: true,
},
Responses: openswag.Responses{
201: {Description: "Task created"},
401: {Description: "Unauthorized"},
},
},
// v2 replaces hard DELETE with soft-delete (breaking change)
{
Method: "POST",
Path: "/tasks/{id}/archive",
Summary: "Archive a task",
Tags: []string{"Tasks"},
Security: []string{openswag.SecurityBearerAuth},
Parameters: []openswag.Parameter{
{Name: "id", In: "path", Required: true, Description: "Task ID"},
},
Responses: openswag.Responses{
200: {Description: "Task archived"},
401: {Description: "Unauthorized"},
},
},
// v2 adds a new endpoint
{
Method: "GET",
Path: "/tasks/{id}/history",
Summary: "Get task history",
Tags: []string{"Tasks"},
Parameters: []openswag.Parameter{
{Name: "id", In: "path", Required: true, Description: "Task ID"},
},
Responses: openswag.Responses{
200: {Description: "Task change history"},
},
},
}
v2Docs := openswag.New(v2Config)
v2Docs.AddAll(v2Endpoints...)
// --- Compare the two specs (Compare takes decoded specs) ---
v1JSON, _ := v1Docs.SpecJSON()
v2JSON, _ := v2Docs.SpecJSON()
var v1Map, v2Map map[string]interface{}
json.Unmarshal(v1JSON, &v1Map)
json.Unmarshal(v2JSON, &v2Map)
differ := versioning.NewDiffer()
diff, err := differ.Compare(v1Map, v2Map)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Total changes: %d\n", len(diff.Changes))
fmt.Printf("Breaking changes: %d\n", len(diff.Breaking))
// --- Generate changelog ---
changelog := versioning.NewChangelogGenerator().Generate(diff)
fmt.Println("\n" + changelog.ToMarkdown())
// --- Generate migration guide ---
guide := versioning.NewMigrationGenerator().Generate(diff)
fmt.Println(guide.ToMarkdown())
// --- Optionally serve both versions side by side ---
mux := http.NewServeMux()
v1Docs.Mount(mux, "/docs/v1")
v2Docs.Mount(mux, "/docs/v2")
fmt.Println("\nv1 docs: http://localhost:8080/docs/v1")
fmt.Println("v2 docs: http://localhost:8080/docs/v2")
http.ListenAndServe(":8080", mux)
}What Changed Between v1 and v2
| Change | Type | Breaking? |
|---|---|---|
DELETE /tasks/{id} removed | Endpoint removed | Yes |
POST /tasks now requires auth | Security added | Yes |
GET /tasks gained page and limit params | Parameters added | No |
POST /tasks/{id}/archive added | New endpoint | No |
GET /tasks/{id}/history added | New endpoint | No |
Key Concepts
Building specs programmatically
Instead of loading YAML files, this example builds both v1 and v2 specs in code using openswag.New. SpecJSON() returns the generated OpenAPI document as JSON; decode each into a map[string]interface{} and pass them to Differ.Compare.
Differ
Create the differ with versioning.NewDiffer(), then call Compare(oldSpec, newSpec) (or CompareFiles(oldPath, newPath) to read from disk).
Breaking change detection
The differ automatically flags backwards-incompatible changes:
- Removing an endpoint (
DELETE /tasks/{id}) - Adding a security requirement to a previously public endpoint (
POST /tasks) - Removing response fields or changing types
Changelog generation
ChangelogGenerator produces a Markdown changelog grouped by tag. This is useful for release notes or API documentation updates.
Migration guide
MigrationGenerator produces a *MigrationGuide; call ToMarkdown() for step-by-step instructions (with before/after snippets) that API consumers can follow to update their client code.
Serving multiple versions
You can mount both spec versions on the same server at different paths (/docs/v1, /docs/v2) so consumers can compare them side by side in the browser.
CI Pipeline Integration
Use the differ in a CI pipeline to block merges that introduce breaking changes:
diff, _ := differ.Compare(mainSpec, prSpec)
if diff.HasBreakingChanges() {
fmt.Println("❌ Breaking changes detected:")
for _, bc := range diff.Breaking {
fmt.Printf(" %s %s — %s\n", bc.Method, bc.Path, bc.Reason)
}
os.Exit(1)
}
fmt.Println("✅ No breaking changes")Run It
go run main.goThe program prints the diff summary, changelog, and migration guide to stdout, then serves both spec versions at http://localhost:8080/docs/v1 and http://localhost:8080/docs/v2.