Developer Guide
This guide provides comprehensive documentation for developers using oastools as both a library and a command-line tool.
Table of Contents
- Installation
- CLI Usage
- Quick Reference
- Pipeline Support
- Library Usage
- Parser Package
- Validator Package
- Fixer Package
- HTTP Validator Package
- Converter Package
- Joiner Package
- Differ Package
- Generator Package
- Builder Package
- Overlay Package
- Walker Package
- Advanced Patterns
- Parse-Once Pattern
- Package Chaining
- Error Handling
- Working with Different OAS Versions
- Troubleshooting
Installation
CLI Tool
# Homebrew (macOS and Linux)
brew install erraggy/oastools/oastools
# Go install
go install github.com/erraggy/oastools/cmd/oastools@latest
# From source
git clone https://github.com/erraggy/oastools.git
cd oastools
make install
Library
CLI Usage
This section provides quick examples of common CLI operations. For complete documentation including all flags, options, and output formats, see the CLI Reference.
Quick Reference
# Validate
oastools validate openapi.yaml
oastools validate --strict --format json openapi.yaml
# Fix common errors
oastools fix openapi.yaml -o fixed.yaml
oastools fix --infer openapi.yaml -o fixed.yaml # Type inference
# Parse and inspect
oastools parse openapi.yaml
oastools parse --resolve-refs openapi.yaml
# Convert between versions
oastools convert -t 3.0.3 swagger.yaml -o openapi.yaml
oastools convert -t 2.0 openapi.yaml -o swagger.yaml
# Join multiple specs
oastools join -o merged.yaml base.yaml ext.yaml
oastools join --path-strategy accept-left -o merged.yaml base.yaml ext.yaml
# Compare specs (breaking change detection)
oastools diff api-v1.yaml api-v2.yaml
oastools diff --breaking api-v1.yaml api-v2.yaml
# Generate Go code
oastools generate --client -o ./client -p petstore openapi.yaml
oastools generate --server -o ./server -p petstore openapi.yaml
# Apply overlay transformations
oastools overlay apply -s openapi.yaml -o result.yaml changes.yaml
oastools overlay validate overlay.yaml
oastools overlay apply --dry-run -s openapi.yaml changes.yaml
Pipeline Support
All commands support stdin (-) and quiet mode (-q) for shell pipelines:
# Fix then validate
oastools fix api.yaml | oastools validate -q -
# Convert via pipeline
cat swagger.yaml | oastools convert -q -t 3.0.3 - > openapi.yaml
# Chain operations
curl -s https://example.com/swagger.yaml | oastools convert -q -t 3.0.3 - | oastools validate -q -
For detailed documentation on each command including all flags, output formats, exit codes, and examples, see the CLI Reference.
Library Usage
Parser Package
The parser package provides parsing for OpenAPI Specification documents.
Basic Parsing:
import "github.com/erraggy/oastools/parser"
// Using functional options (recommended for simple cases)
result, err := parser.ParseWithOptions(
parser.WithFilePath("openapi.yaml"),
parser.WithValidateStructure(true),
)
if err != nil {
log.Fatal(err)
}
// Access parsed document (version-specific)
switch doc := result.Document.(type) {
case *parser.OAS2Document:
fmt.Printf("Swagger %s: %s\n", doc.Swagger, doc.Info.Title)
case *parser.OAS3Document:
fmt.Printf("OpenAPI %s: %s\n", doc.OpenAPI, doc.Info.Title)
}
// Or use version-agnostic access with DocumentAccessor
if accessor := result.AsAccessor(); accessor != nil {
fmt.Printf("API: %s\n", accessor.GetInfo().Title)
fmt.Printf("Schemas: %d\n", len(accessor.GetSchemas()))
}
Reusable Parser Instance:
// Create a reusable parser for processing multiple files
p := parser.New()
p.ResolveRefs = false
p.ValidateStructure = true
// Process multiple files with same configuration
result1, _ := p.Parse("api1.yaml")
result2, _ := p.Parse("api2.yaml")
result3, _ := p.Parse("api3.yaml")
Parsing from Different Sources:
// From file
result, err := parser.ParseWithOptions(
parser.WithFilePath("openapi.yaml"),
)
// From URL
result, err := parser.ParseWithOptions(
parser.WithFilePath("https://example.com/openapi.yaml"),
)
// From bytes
yamlContent := []byte(`openapi: "3.0.0"
info:
title: My API
version: "1.0"
paths: {}`)
result, err := parser.ParseWithOptions(
parser.WithBytes(yamlContent),
)
// From io.Reader
file, _ := os.Open("openapi.yaml")
result, err := parser.ParseWithOptions(
parser.WithReader(file),
)
Working with External References:
// Enable reference resolution
result, err := parser.ParseWithOptions(
parser.WithFilePath("openapi.yaml"),
parser.WithResolveRefs(true),
)
// Security: References are restricted to base directory and subdirectories
// HTTP(S) references are NOT supported for security reasons
Order-Preserving Marshaling:
// Enable order preservation for deterministic output
result, err := parser.ParseWithOptions(
parser.WithFilePath("openapi.yaml"),
parser.WithPreserveOrder(true),
)
// Marshal with original field order preserved
jsonBytes, _ := result.MarshalOrderedJSON()
yamlBytes, _ := result.MarshalOrderedYAML()
This is useful for hash-based caching, minimizing diffs, and CI pipelines that compare serialized output.
OAS 3.2.0 and JSON Schema 2020-12 Features:
// Access OAS 3.2.0 specific fields
if doc, ok := result.OAS3Document(); ok {
// Document identity ($self)
if doc.Self != "" {
fmt.Printf("Document identity: %s\n", doc.Self)
}
// QUERY method and custom HTTP methods
for path, pathItem := range doc.Paths {
if pathItem.Query != nil {
fmt.Printf("QUERY at %s\n", path)
}
for method := range pathItem.AdditionalOperations {
fmt.Printf("Custom %s at %s\n", method, path)
}
}
// JSON Schema 2020-12 keywords (polymorphic types)
if doc.Components != nil && doc.Components.Schemas != nil {
if schema, ok := doc.Components.Schemas["Example"]; ok {
switch v := schema.UnevaluatedProperties.(type) {
case *parser.Schema:
fmt.Println("Unevaluated props must match schema")
case bool:
fmt.Printf("Unevaluated props allowed: %v\n", v)
}
}
}
}
For comprehensive coverage of these features, see Parser Deep Dive.
Validator Package
The validator package provides validation for OpenAPI Specification documents.
Basic Validation:
import "github.com/erraggy/oastools/validator"
// Validate with functional options
result, err := validator.ValidateWithOptions(
validator.WithFilePath("openapi.yaml"),
validator.WithIncludeWarnings(true),
)
if err != nil {
log.Fatal(err)
}
if !result.Valid {
fmt.Printf("Validation failed with %d errors\n", result.ErrorCount)
for _, err := range result.Errors {
fmt.Printf(" %s: %s\n", err.Path, err.Message)
}
}
Strict Mode:
// Strict mode treats warnings as errors
result, err := validator.ValidateWithOptions(
validator.WithFilePath("openapi.yaml"),
validator.WithIncludeWarnings(true),
validator.WithStrictMode(true),
)
// In strict mode, result.Valid will be false if there are warnings
Validating Pre-Parsed Documents:
// Parse once, validate multiple times (30x faster)
parseResult, err := parser.ParseWithOptions(
parser.WithFilePath("openapi.yaml"),
parser.WithValidateStructure(true),
)
result, err := validator.ValidateWithOptions(
validator.WithParsed(*parseResult),
validator.WithIncludeWarnings(true),
)
Processing Validation Errors:
for _, err := range result.Errors {
fmt.Printf("Path: %s\n", err.Path)
fmt.Printf("Message: %s\n", err.Message)
fmt.Printf("Severity: %s\n", err.Severity)
if err.SpecRef != "" {
fmt.Printf("Spec Reference: %s\n", err.SpecRef)
}
}
Accessing Operation Context:
// Each validation error includes operation context when applicable
for _, err := range result.Errors {
if err.OperationContext != nil {
fmt.Printf("Operation: %s %s\n", err.OperationContext.Method, err.OperationContext.Path)
if err.OperationContext.OperationID != "" {
fmt.Printf("OperationId: %s\n", err.OperationContext.OperationID)
}
}
fmt.Printf("Error: %s\n", err.Message)
}
// The String() method automatically includes operation context
fmt.Println(err.String()) // "GET /users/{id} (getUser): parameter 'id' is required"
📚 Deep Dive: For comprehensive examples and advanced patterns, see the Validator Deep Dive.
Fixer Package
The fixer package automatically corrects common validation errors in OpenAPI Specification documents.
Basic Fixing:
import "github.com/erraggy/oastools/fixer"
// Fix with functional options
result, err := fixer.FixWithOptions(
fixer.WithFilePath("openapi.yaml"),
fixer.WithInferTypes(true),
)
if err != nil {
log.Fatal(err)
}
if result.HasFixes() {
fmt.Printf("Applied %d fixes\n", result.FixCount)
for _, fix := range result.Fixes {
fmt.Printf(" %s: %s\n", fix.Type, fix.Description)
}
}
Reusable Fixer Instance:
// Create a reusable fixer for processing multiple files
f := fixer.New()
f.InferTypes = true
// Process multiple files with same configuration
result1, _ := f.Fix("api1.yaml")
result2, _ := f.Fix("api2.yaml")
result3, _ := f.Fix("api3.yaml")
Fixing Pre-Parsed Documents:
// Parse once, fix multiple times
parseResult, err := parser.ParseWithOptions(
parser.WithFilePath("openapi.yaml"),
parser.WithValidateStructure(true),
)
result, err := fixer.FixWithOptions(
fixer.WithParsed(*parseResult),
fixer.WithInferTypes(true),
)
Processing Fix Results:
result, _ := fixer.FixWithOptions(
fixer.WithFilePath("openapi.yaml"),
)
for _, fix := range result.Fixes {
fmt.Printf("Type: %s\n", fix.Type)
fmt.Printf("Path: %s\n", fix.Path)
fmt.Printf("Description: %s\n", fix.Description)
}
// Access the fixed document
switch doc := result.Document.(type) {
case *parser.OAS2Document:
// Work with fixed OAS 2.0 document
case *parser.OAS3Document:
// Work with fixed OAS 3.x document
}
Fixing Invalid Schema Names:
Note: Schema renaming is not enabled by default. You must use
WithEnabledFixes()to opt-in.
// Fix schemas with invalid names (e.g., Response[User])
result, err := fixer.FixWithOptions(
fixer.WithFilePath("openapi.yaml"),
fixer.WithEnabledFixes(fixer.FixTypeRenamedGenericSchema), // Enable schema renaming
fixer.WithGenericNaming(fixer.GenericNamingOf), // Response[User] → ResponseOfUser
)
// Available naming strategies:
// - fixer.GenericNamingUnderscore: Response[User] → Response_User_
// - fixer.GenericNamingOf: Response[User] → ResponseOfUser
// - fixer.GenericNamingFor: Response[User] → ResponseForUser
// - fixer.GenericNamingFlattened: Response[User] → ResponseUser
// - fixer.GenericNamingDot: Response[User] → Response.User
Pruning Unused Schemas:
// Remove unreferenced schemas
result, err := fixer.FixWithOptions(
fixer.WithFilePath("openapi.yaml"),
fixer.WithEnabledFixes(fixer.FixTypePrunedUnusedSchema),
)
for _, fix := range result.Fixes {
fmt.Printf("Removed unused schema: %s\n", fix.Before)
}
Fixing Duplicate OperationIds:
// Fix duplicate operationId values with default template
result, err := fixer.FixWithOptions(
fixer.WithFilePath("openapi.yaml"),
fixer.WithEnabledFixes(fixer.FixTypeDuplicateOperationId),
)
// With custom naming template
result, err := fixer.FixWithOptions(
fixer.WithFilePath("openapi.yaml"),
fixer.WithEnabledFixes(fixer.FixTypeDuplicateOperationId),
fixer.WithOperationIdNamingConfig(fixer.OperationIdNamingConfig{
Template: "{method:pascal}{path:pascal}",
}),
)
// Template placeholders: {operationId}, {method}, {path}, {tag}, {tags}, {n}
// Modifiers: :pascal, :camel, :snake, :kebab, :upper, :lower
// Default template: {operationId}{n} produces getUser, getUser2, getUser3, etc.
Stubbing Missing References:
// Create stubs for unresolved local $ref pointers
result, err := fixer.FixWithOptions(
fixer.WithFilePath("openapi.yaml"),
fixer.WithEnabledFixes(fixer.FixTypeStubMissingRef),
)
// With custom response description
result, err := fixer.FixWithOptions(
fixer.WithFilePath("openapi.yaml"),
fixer.WithEnabledFixes(fixer.FixTypeStubMissingRef),
fixer.WithStubResponseDescription("TODO: implement"),
)
// Missing schema references get empty {} stubs
// Missing response references get stub responses with configurable descriptions
Dry-Run Mode:
// Preview changes without applying
result, err := fixer.FixWithOptions(
fixer.WithFilePath("openapi.yaml"),
fixer.WithDryRun(true),
)
fmt.Printf("Would apply %d fix(es)\n", result.FixCount)
for _, fix := range result.Fixes {
fmt.Printf(" %s: %s\n", fix.Type, fix.Description)
}
Mutable Input (Skip Defensive Copy):
By default, FixWithOptions deep-copies the parsed document to avoid mutating the caller's data. When you own the input and don't need it afterward, use WithMutableInput(true) to skip the copy and fix in place:
// Caller owns the document and doesn't need the original
result, err := fixer.FixWithOptions(
fixer.WithParsed(*parseResult),
fixer.WithMutableInput(true), // Skip defensive copy
)
This is especially useful when chaining fixer passes, since each intermediate result is a fresh document you already own:
// First pass produces a new document
pass1, _ := fixer.FixWithOptions(
fixer.WithFilePath("openapi.yaml"),
fixer.WithEnabledFixes(fixer.FixTypeMissingPathParameter),
)
// Second pass can mutate pass1's document directly
pass2, _ := fixer.FixWithOptions(
fixer.WithParsed(*pass1.ToParseResult()),
fixer.WithMutableInput(true), // Safe - we own pass1's document
fixer.WithEnabledFixes(fixer.FixTypeRenamedGenericSchema),
)
HTTP Validator Package
The httpvalidator package validates HTTP requests and responses against OpenAPI specifications at runtime, enabling API gateways, middleware, and testing frameworks to enforce API contracts.
Basic Request Validation:
import (
"net/http"
"github.com/erraggy/oastools/httpvalidator"
"github.com/erraggy/oastools/parser"
)
// Parse specification once at startup
parsed, _ := parser.ParseWithOptions(
parser.WithFilePath("openapi.yaml"),
)
// Create validator instance
v, _ := httpvalidator.New(parsed)
// Validate incoming request
req, _ := http.NewRequest("GET", "/users/123?page=1", nil)
result, err := v.ValidateRequest(req)
if result.Valid {
// Access deserialized parameters
userID := result.PathParams["userId"] // "123"
page := result.QueryParams["page"] // 1 (int)
}
Response Validation (for middleware):
// Validate response using captured response data
result, _ := v.ValidateResponseData(
req,
statusCode,
responseHeaders,
responseBody,
)
if !result.Valid {
log.Printf("Response validation failed: %v", result.Errors)
}
Strict Mode:
// Enable strict validation
v.StrictMode = true // Reject unknown parameters and undocumented responses
result, _ := v.ValidateRequest(req)
// Unknown query parameters will cause validation errors
Functional Options API:
// One-off validation without creating a validator instance
result, err := httpvalidator.ValidateRequestWithOptions(
req,
httpvalidator.WithFilePath("openapi.yaml"),
httpvalidator.WithStrictMode(true),
)
For comprehensive httpvalidator usage patterns, see the HTTP Validator Deep Dive.
Converter Package
The converter package provides version conversion for OpenAPI Specification documents.
Basic Conversion:
import "github.com/erraggy/oastools/converter"
// Convert OAS 2.0 to 3.0.3
result, err := converter.ConvertWithOptions(
converter.WithFilePath("swagger.yaml"),
converter.WithTargetVersion("3.0.3"),
)
if err != nil {
log.Fatal(err)
}
// Check for critical issues
if result.HasCriticalIssues() {
fmt.Printf("Conversion had %d critical issues\n", result.CriticalCount)
for _, issue := range result.Issues {
if issue.Severity == converter.SeverityCritical {
fmt.Printf(" [CRITICAL] %s: %s\n", issue.Path, issue.Message)
}
}
}
Handling Conversion Issues:
result, err := converter.ConvertWithOptions(
converter.WithFilePath("swagger.yaml"),
converter.WithTargetVersion("3.0.3"),
converter.WithIncludeInfo(true), // Include informational messages
)
// Process issues by severity
for _, issue := range result.Issues {
switch issue.Severity {
case converter.SeverityCritical:
fmt.Printf("CRITICAL: %s - %s\n", issue.Path, issue.Message)
if issue.Context != "" {
fmt.Printf(" Context: %s\n", issue.Context)
}
case converter.SeverityWarning:
fmt.Printf("WARNING: %s - %s\n", issue.Path, issue.Message)
case converter.SeverityInfo:
fmt.Printf("INFO: %s - %s\n", issue.Path, issue.Message)
}
}
Writing Converted Output:
import (
"os"
"encoding/json"
"go.yaml.in/yaml/v4"
)
result, err := converter.ConvertWithOptions(
converter.WithFilePath("swagger.yaml"),
converter.WithTargetVersion("3.0.3"),
)
// Write as YAML (preserves input format by default)
var output []byte
if result.SourceFormat == parser.SourceFormatJSON {
output, _ = json.MarshalIndent(result.Document, "", " ")
} else {
output, _ = yaml.Marshal(result.Document)
}
os.WriteFile("openapi.yaml", output, 0600)
Joiner Package
The joiner package provides joining for multiple OpenAPI Specification documents.
Basic Joining:
import "github.com/erraggy/oastools/joiner"
// Join with default configuration
result, err := joiner.JoinWithOptions(
joiner.WithFilePaths([]string{"base.yaml", "extension.yaml"}),
)
if err != nil {
log.Fatal(err)
}
// Write result to file
joiner.WriteResult(result, "merged.yaml")
Custom Collision Strategies:
// Different strategies for different component types
config := joiner.JoinerConfig{
DefaultStrategy: joiner.StrategyFailOnCollision,
PathStrategy: joiner.StrategyFailOnPaths, // Fail on path collisions
SchemaStrategy: joiner.StrategyAcceptLeft, // Keep first schema
ComponentStrategy: joiner.StrategyAcceptRight, // Keep last component
DeduplicateTags: true,
MergeArrays: true,
}
result, err := joiner.JoinWithOptions(
joiner.WithFilePaths([]string{"base.yaml", "ext.yaml"}),
joiner.WithConfig(config),
)
Joining Pre-Parsed Documents (154x faster):
// Parse documents once
docs := make([]parser.ParseResult, 0)
for _, path := range []string{"api1.yaml", "api2.yaml", "api3.yaml"} {
result, err := parser.ParseWithOptions(
parser.WithFilePath(path),
parser.WithValidateStructure(true),
)
if err != nil {
log.Fatal(err)
}
docs = append(docs, *result)
}
// Join parsed documents
config := joiner.DefaultConfig()
j := joiner.New(config)
result, err := j.JoinParsed(docs)
Processing Join Warnings:
result, err := joiner.JoinWithOptions(
joiner.WithFilePaths([]string{"base.yaml", "ext.yaml"}),
joiner.WithConfig(joiner.DefaultConfig()),
)
if len(result.Warnings) > 0 {
fmt.Printf("Join completed with %d warnings:\n", len(result.Warnings))
for _, warning := range result.Warnings {
fmt.Printf(" - %s\n", warning)
}
}
if result.CollisionCount > 0 {
fmt.Printf("Resolved %d collisions\n", result.CollisionCount)
}
Semantic Deduplication:
Semantic deduplication identifies structurally identical schemas across documents and consolidates them to reduce duplication:
// Enable semantic deduplication to consolidate identical schemas
config := joiner.JoinerConfig{
DefaultStrategy: joiner.StrategyAcceptLeft,
SemanticDeduplication: true, // Enable schema deduplication
EquivalenceMode: "deep", // Use deep structural comparison
DeduplicateTags: true,
MergeArrays: true,
}
j := joiner.New(config)
result, err := j.Join([]string{"api1.yaml", "api2.yaml", "api3.yaml"})
if err != nil {
log.Fatal(err)
}
// Semantic deduplication consolidates identical schemas:
// - Schemas with identical structure are detected via FNV-1a hashing
// - The alphabetically-first name becomes the canonical schema
// - All $ref references are automatically rewritten
// - Warnings indicate how many duplicates were consolidated
Operation-Aware Schema Renaming:
When joining programmatically generated specs (e.g., from frameworks like Huma), schemas often have generic names like "Response" or "Request". Operation-aware renaming uses API structure context to generate meaningful names:
// Enable operation context for rich rename templates
config := joiner.DefaultConfig()
config.SchemaStrategy = joiner.StrategyRenameRight
config.OperationContext = true // Build reference graph
config.RenameTemplate = "{{.OperationID | pascalCase}}{{.Name}}"
j := joiner.New(config)
result, err := j.Join([]string{"users-api.yaml", "orders-api.yaml"})
// Colliding "Response" schemas become:
// - Response (from users-api, kept)
// - CreateOrderResponse (from orders-api, renamed using operationId)
Template functions available:
- Path extraction:
pathResource(last non-parameter segment),pathLast(final segment) - Case conversion:
pascalCase,camelCase,snakeCase,kebabCase - Conditionals:
default(fallback value),coalesce(first non-empty)
Example template using path context:
config.RenameTemplate = "{{.Path | pathResource | pascalCase}}{{.Name}}"
// /orders/{id} + "Response" → OrdersResponse
Collision Handler Callbacks:
For fine-grained control over collision resolution, register a collision handler callback. The handler is invoked for each collision with full context, allowing custom decisions or observation:
// Observe-only handler: log collisions but use configured strategy
result, err := joiner.JoinWithOptions(
joiner.WithFilePaths([]string{"users-api.yaml", "billing-api.yaml"}),
joiner.WithCollisionHandler(func(c joiner.CollisionContext) (joiner.CollisionResolution, error) {
log.Printf("Collision detected: %s %q (%s vs %s)\n",
c.Type, c.Name, c.LeftSource, c.RightSource)
return joiner.ContinueWithStrategy(), nil // Defer to configured strategy
}),
)
Handle only specific collision types:
// Only handle schema collisions, let others use configured strategy
result, err := joiner.JoinWithOptions(
joiner.WithFilePaths([]string{"users-api.yaml", "billing-api.yaml"}),
joiner.WithCollisionHandlerFor(joiner.CollisionTypeSchema, func(c joiner.CollisionContext) (joiner.CollisionResolution, error) {
if c.Name == "Error" {
return joiner.AcceptLeft(), nil // Always keep left Error schema
}
return joiner.ContinueWithStrategy(), nil
}),
)
Resolution actions available:
ContinueWithStrategy()- Defer to the configured collision strategyAcceptLeft()- Keep the value from the left (earlier) documentAcceptRight()- Keep the value from the right (later) documentRename()- Rename the right value using the configured templateDeduplicate()- Merge if structurally equivalent, fail otherwiseFail()- Fail the join operation with an errorUseCustomValue(value)- Use a custom-provided value
The CollisionContext provides: Type, Name, JSONPath, LeftSource/RightSource, LeftValue/RightValue, semantic hashes, and AreEquivalent for quick equivalence checks.
📚 Deep Dive: For comprehensive examples and advanced patterns, see the Joiner Deep Dive.
Differ Package
The differ package provides OpenAPI specification comparison and breaking change detection.
Basic Diff:
import "github.com/erraggy/oastools/differ"
// Simple diff
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())
}
Breaking Change Detection:
// Enable breaking change detection
result, err := differ.DiffWithOptions(
differ.WithSourceFilePath("api-v1.yaml"),
differ.WithTargetFilePath("api-v2.yaml"),
differ.WithMode(differ.ModeBreaking),
differ.WithIncludeInfo(true),
)
if result.HasBreakingChanges {
fmt.Printf("⚠️ Found %d breaking changes!\n", result.BreakingCount)
}
fmt.Printf("Summary: %d breaking, %d warnings, %d info\n",
result.BreakingCount, result.WarningCount, result.InfoCount)
Diffing Pre-Parsed Documents (81x faster):
// Parse documents once
source, _ := parser.ParseWithOptions(
parser.WithFilePath("api-v1.yaml"),
parser.WithValidateStructure(true),
)
target, _ := parser.ParseWithOptions(
parser.WithFilePath("api-v2.yaml"),
parser.WithValidateStructure(true),
)
// Compare parsed documents
result, err := differ.DiffWithOptions(
differ.WithSourceParsed(*source),
differ.WithTargetParsed(*target),
differ.WithMode(differ.ModeBreaking),
)
Grouping Changes by Category:
result, _ := differ.DiffWithOptions(
differ.WithSourceFilePath("api-v1.yaml"),
differ.WithTargetFilePath("api-v2.yaml"),
differ.WithMode(differ.ModeBreaking),
)
// Group changes by category
categories := make(map[differ.ChangeCategory][]differ.Change)
for _, change := range result.Changes {
categories[change.Category] = append(categories[change.Category], change)
}
// Process each category
categoryOrder := []differ.ChangeCategory{
differ.CategoryEndpoint,
differ.CategoryOperation,
differ.CategoryParameter,
differ.CategoryRequestBody,
differ.CategoryResponse,
differ.CategorySchema,
differ.CategorySecurity,
differ.CategoryServer,
differ.CategoryInfo,
}
for _, category := range categoryOrder {
changes := categories[category]
if len(changes) > 0 {
fmt.Printf("\n%s Changes (%d):\n", category, len(changes))
for _, change := range changes {
fmt.Printf(" %s\n", change.String())
}
}
}
Configurable Breaking Change Rules:
// Customize which changes are considered breaking
rules := &differ.BreakingRulesConfig{
Operation: &differ.OperationRules{
// Downgrade operationId changes from error to info
OperationIDModified: &differ.BreakingChangeRule{
Severity: differ.SeverityPtr(differ.SeverityInfo),
},
},
Schema: &differ.SchemaRules{
// Ignore type changes entirely
TypeModified: &differ.BreakingChangeRule{
Ignore: true,
},
},
}
result, err := differ.DiffWithOptions(
differ.WithSourceFilePath("v1.yaml"),
differ.WithTargetFilePath("v2.yaml"),
differ.WithBreakingRules(rules),
)
// Or use preset configurations:
// - differ.DefaultRules() - built-in defaults
// - differ.StrictRules() - elevates warnings to errors
// - differ.LenientRules() - downgrades some errors to warnings
📚 Deep Dive: For comprehensive examples and advanced patterns, see the Differ Deep Dive.
Generator Package
The generator package creates idiomatic Go code for API clients and server stubs from OpenAPI specifications.
Basic Code Generation:
import "github.com/erraggy/oastools/generator"
// Generate client and server code
result, err := generator.GenerateWithOptions(
generator.WithFilePath("openapi.yaml"),
generator.WithPackageName("petstore"),
generator.WithClient(true),
generator.WithServer(true),
)
if err != nil {
log.Fatal(err)
}
// Write generated files to output directory
if err := result.WriteFiles("./generated"); err != nil {
log.Fatal(err)
}
fmt.Printf("Generated %d files\n", len(result.Files))
fmt.Printf("Types: %d, Operations: %d\n", result.GeneratedTypes, result.GeneratedOperations)
Types-Only Generation:
// Generate only type definitions from schemas
result, err := generator.GenerateWithOptions(
generator.WithFilePath("openapi.yaml"),
generator.WithPackageName("models"),
generator.WithTypes(true),
generator.WithClient(false),
generator.WithServer(false),
)
Configuration Options:
g := generator.New()
g.PackageName = "api"
g.GenerateClient = true
g.GenerateServer = true
g.GenerateTypes = true // Always true when client or server enabled
g.UsePointers = true // Use pointers for optional fields
g.IncludeValidation = true // Add validation tags
g.StrictMode = false // Fail on generation issues
g.IncludeInfo = true // Include info messages
result, err := g.Generate("openapi.yaml")
if err != nil {
log.Fatal(err)
}
// Access generated files
for _, file := range result.Files {
fmt.Printf("%s: %d bytes\n", file.Name, len(file.Content))
}
// Check for critical issues
if result.HasCriticalIssues() {
fmt.Printf("Warning: %d critical issue(s)\n", result.CriticalCount)
}
Handling Generation Issues:
result, err := generator.GenerateWithOptions(
generator.WithFilePath("openapi.yaml"),
generator.WithPackageName("api"),
generator.WithClient(true),
)
if err != nil {
log.Fatal(err)
}
// Process issues by severity
for _, issue := range result.Issues {
switch issue.Severity {
case generator.SeverityCritical:
fmt.Printf("CRITICAL [%s]: %s\n", issue.Path, issue.Message)
case generator.SeverityWarning:
fmt.Printf("WARNING [%s]: %s\n", issue.Path, issue.Message)
case generator.SeverityInfo:
fmt.Printf("INFO [%s]: %s\n", issue.Path, issue.Message)
}
}
Security Code Generation:
// Generate client with security helpers
result, err := generator.GenerateWithOptions(
generator.WithFilePath("openapi.yaml"),
generator.WithPackageName("api"),
generator.WithClient(true),
generator.WithSecurity(true), // Generate security helpers
)
// The generated security_helpers.go contains ClientOption functions:
// - WithApiKeyAPIKey(key string) for apiKey (header) schemes
// - WithApiKeyAPIKeyQuery(key string) for apiKey (query) schemes
// - WithApiKeyAPIKeyCookie(key string) for apiKey (cookie) schemes
// - WithBasicAuthBasicAuth(username, password string) for HTTP basic auth
// - WithBearerTokenBearerToken(token string) for HTTP bearer auth
// - WithOAuth2OAuth2Token(token string) for OAuth2
// - WithOidcToken(token string) for OpenID Connect
OAuth2 Flow Generation:
// Generate full OAuth2 client implementations
result, err := generator.GenerateWithOptions(
generator.WithFilePath("openapi.yaml"),
generator.WithPackageName("api"),
generator.WithClient(true),
generator.WithOAuth2Flows(true),
)
// Generated OAuth2 code includes:
// - {SchemeName}OAuth2Config struct
// - {SchemeName}OAuth2Client with flow methods
// - GetAuthorizationURL() for authorization code flow
// - ExchangeCode() to exchange codes for tokens
// - GeneratePKCEChallenge() for PKCE challenge generation
// - GetAuthorizationURLWithPKCE() for secure authorization with PKCE
// - ExchangeCodeWithPKCE() for token exchange with PKCE
// - GetClientCredentialsToken() for client credentials
// - RefreshToken() for token refresh
// - WithOAuth2AutoRefresh() ClientOption
Credential Management:
// Generate credential provider interfaces
result, err := generator.GenerateWithOptions(
generator.WithFilePath("openapi.yaml"),
generator.WithPackageName("api"),
generator.WithClient(true),
generator.WithCredentialMgmt(true),
)
// Generated credential code includes:
// - CredentialProvider interface
// - MemoryCredentialProvider for testing
// - EnvCredentialProvider for environment variables
// - CredentialChain for fallback providers
// - WithCredentialProvider() ClientOption
Security Enforcement (Server-Side):
// Generate security validation middleware
result, err := generator.GenerateWithOptions(
generator.WithFilePath("openapi.yaml"),
generator.WithPackageName("api"),
generator.WithServer(true),
generator.WithSecurityEnforce(true),
)
// Generated enforcement code includes:
// - SecurityRequirement struct
// - OperationSecurityRequirements map
// - SecurityValidator for validating requests
// - RequireSecurityMiddleware for enforcement
OpenID Connect Discovery:
// Generate OIDC discovery client
result, err := generator.GenerateWithOptions(
generator.WithFilePath("openapi.yaml"),
generator.WithPackageName("api"),
generator.WithClient(true),
generator.WithOIDCDiscovery(true),
)
// Generated OIDC code includes:
// - OIDCConfiguration struct
// - OIDCDiscoveryClient for .well-known discovery
// - NewOAuth2ClientFromOIDC() helper
File Splitting for Large APIs:
// Configure file splitting for large APIs
result, err := generator.GenerateWithOptions(
generator.WithFilePath("large-api.yaml"),
generator.WithPackageName("api"),
generator.WithClient(true),
generator.WithMaxLinesPerFile(2000),
generator.WithMaxTypesPerFile(200),
generator.WithMaxOperationsPerFile(100),
generator.WithSplitByTag(true),
generator.WithSplitByPathPrefix(true),
)
// Files will be split based on:
// 1. Operation tags (e.g., users_client.go, orders_client.go)
// 2. Path prefixes (e.g., api_v1_client.go, api_v2_client.go)
// 3. Shared types in types.go
README Generation:
// Generate documentation with the code
result, err := generator.GenerateWithOptions(
generator.WithFilePath("openapi.yaml"),
generator.WithPackageName("api"),
generator.WithClient(true),
generator.WithReadme(true),
)
// README.md includes:
// - API overview and version
// - Generated file descriptions
// - Security configuration examples
// - Regeneration command
📚 Deep Dive: For comprehensive examples and advanced patterns, see the Generator Deep Dive.
Server Extensions:
Generate a complete server framework with validation, routing, and testing support:
// Generate server with all extensions
result, err := generator.GenerateWithOptions(
generator.WithFilePath("openapi.yaml"),
generator.WithPackageName("api"),
generator.WithServer(true),
generator.WithServerAll(), // Enable all server extensions
)
Server extension options:
WithServerResponses(true): Typed response writers withStatus*()methodsWithServerBinder(true): Request parameter binding using httpvalidatorWithServerMiddleware(true): Validation middleware for request/response validationWithServerRouter("stdlib")orWithServerRouter("chi"): HTTP router generation with path matchingWithServerStubs(true): Configurable stub implementations for testingWithServerEmbedSpec(true): Embed OpenAPI spec for runtime validation
Generated server extension files:
server_responses.go: Per-operation response types withWriteTo()methodsserver_binder.go:RequestBinderwithBind{Operation}Request()methodsserver_middleware.go:ValidationMiddlewarewith configurable error handlingserver_router.go:ServerRouterimplementinghttp.Handlerserver_stubs.go:StubServerwith configurable function fields for testing
Example router setup with error logging:
// Create router with validation middleware and error logging
middleware, _ := ValidationMiddleware(parsed)
router, _ := NewServerRouter(server, parsed,
WithMiddleware(middleware),
WithErrorHandler(func(r *http.Request, err error) {
log.Printf("Handler error: %s %s: %v", r.Method, r.URL.Path, err)
}),
)
http.ListenAndServe(":8080", router)
Chi router alternative: For projects using chi, use WithServerRouter("chi"):
result, _ := generator.GenerateWithOptions(
generator.WithFilePath("openapi.yaml"),
generator.WithPackageName("api"),
generator.WithServer(true),
generator.WithServerRouter("chi"), // Generate chi-based router
)
The chi router provides native path parameter extraction via chi.URLParam() and integrates with chi's middleware ecosystem.
Builder Package
The builder package enables programmatic construction of OpenAPI specifications with reflection-based schema generation from Go types.
Basic Construction:
import (
"net/http"
"github.com/erraggy/oastools/builder"
"github.com/erraggy/oastools/parser"
)
// Define Go types for your API
type User struct {
ID int64 `json:"id" oas:"description=Unique user identifier"`
Name string `json:"name" oas:"minLength=1,maxLength=100"`
Email string `json:"email" oas:"format=email"`
}
type Error struct {
Code int `json:"code" oas:"description=HTTP status code"`
Message string `json:"message" oas:"description=Error message"`
}
// Build OAS 3.0 specification
spec := builder.New(parser.OASVersion300).
SetTitle("User API").
SetVersion("1.0.0").
SetDescription("Simple user management API").
AddOperation(http.MethodGet, "/users/{id}",
builder.WithOperationID("getUserByID"),
builder.WithOperationDescription("Get a user by ID"),
builder.WithParameter("id", "path", "string", "User ID"),
builder.WithResponse(http.StatusOK, User{}),
builder.WithResponse(http.StatusNotFound, Error{}),
).
AddOperation(http.MethodPost, "/users",
builder.WithOperationID("createUser"),
builder.WithRequestBody(User{}, "Create user request"),
builder.WithResponse(http.StatusCreated, User{}),
builder.WithResponse(http.StatusBadRequest, Error{}),
)
// Build the document
doc, err := spec.BuildOAS3()
if err != nil {
log.Fatal(err)
}
// Convert to YAML/JSON
data, _ := parser.ToYAML(doc)
fmt.Println(string(data))
OAS Version Selection:
// Build for OAS 3.2.0 (latest)
spec := builder.New(parser.OASVersion320)
// Build for OAS 3.1.x
spec := builder.New(parser.OASVersion310)
// Build for OAS 3.0.x
spec := builder.New(parser.OASVersion300)
// Build for OAS 2.0 (Swagger)
spec := builder.New(parser.OASVersion20)
Schema Generation from Go Types:
// Builder automatically generates JSON Schema from Go types
// Struct tags control schema properties:
type Product struct {
ID int64 `json:"id" oas:"description=Product ID"`
Name string `json:"name" oas:"minLength=1,maxLength=255"`
Price float64 `json:"price" oas:"minimum=0,exclusiveMinimum=true"`
Description string `json:"description" oas:"maxLength=1000"`
Tags []string `json:"tags" oas:"maxItems=10"`
Active bool `json:"active" oas:"description=Is product active"`
CreatedAt time.Time `json:"created_at" oas:"format=date-time"`
}
// Builder generates appropriate OpenAPI 3.0 schema:
// - Infers types from struct field types
// - Applies constraints from oas tags
// - Handles nested structs, arrays, and time.Time
// - Generates descriptions and format hints
Semantic Deduplication:
Enable semantic deduplication to automatically consolidate structurally identical schemas:
import (
"net/http"
"github.com/erraggy/oastools/builder"
"github.com/erraggy/oastools/parser"
)
// Types that are structurally identical
type UserID struct {
Value int64 `json:"value"`
}
type CustomerID struct {
Value int64 `json:"value"`
}
// Enable semantic deduplication
spec := builder.New(parser.OASVersion320,
builder.WithSemanticDeduplication(true),
).
SetTitle("API").
SetVersion("1.0.0").
AddOperation(http.MethodGet, "/users/{id}",
builder.WithResponse(http.StatusOK, UserID{}),
).
AddOperation(http.MethodGet, "/customers/{id}",
builder.WithResponse(http.StatusOK, CustomerID{}),
)
doc, err := spec.BuildOAS3()
// Result: Only 1 schema instead of 2, with $refs automatically rewritten
Custom Field Processors:
For libraries that need to support custom struct tag conventions (e.g., migrating from other OpenAPI libraries), use WithSchemaFieldProcessor:
// Support legacy standalone tags like `description:"..."`
processor := func(schema *parser.Schema, field reflect.StructField) *parser.Schema {
if desc := field.Tag.Get("description"); desc != "" {
schema.Description = desc
}
return schema
}
spec := builder.New(parser.OASVersion320,
builder.WithSchemaFieldProcessor(processor),
)
Multiple processors can be composed:
composed := builder.ComposeSchemaFieldProcessors(descProcessor, enumProcessor)
spec := builder.New(parser.OASVersion320, builder.WithSchemaFieldProcessor(composed))
Vendor Extensions:
// Add vendor extensions (x-* fields) to operations, parameters, responses, and request bodies
spec.AddOperation(http.MethodGet, "/users",
builder.WithOperationExtension("x-rate-limit", 100),
builder.WithQueryParam("limit", int32(0),
builder.WithParamExtension("x-ui-widget", "slider"),
),
builder.WithResponse(http.StatusOK, []User{},
builder.WithResponseExtension("x-cache-ttl", 3600),
),
)
spec.AddOperation(http.MethodPost, "/users",
builder.WithRequestBody("application/json", User{},
builder.WithRequestBodyExtension("x-codegen-request-body-name", "user"),
),
)
OAS 2.0 Specific Options:
// OAS 2.0 supports operation-level consumes/produces and special parameter options
spec := builder.New(parser.OASVersion20).
SetTitle("API").
SetVersion("1.0.0").
AddOperation(http.MethodPost, "/users",
builder.WithConsumes("application/json", "application/xml"),
builder.WithProduces("application/json"),
builder.WithQueryParam("tags", []string{},
builder.WithParamCollectionFormat("csv"), // csv, ssv, tsv, pipes, multi
builder.WithParamAllowEmptyValue(true),
),
builder.WithRequestBody("application/json", User{}),
builder.WithResponse(http.StatusOK, User{}),
)
Explicit Type and Format Overrides:
// Override inferred type/format when Go types don't map directly to desired OpenAPI schema
spec.AddOperation(http.MethodGet, "/users/{user_id}",
builder.WithPathParam("user_id", "",
builder.WithParamFormat("uuid"), // String with UUID format
),
builder.WithQueryParam("version", 0,
builder.WithParamType("integer"),
builder.WithParamFormat("int64"),
),
)
// Full schema override for complex types
spec.AddOperation(http.MethodGet, "/items",
builder.WithQueryParam("ids", nil,
builder.WithParamSchema(&parser.Schema{
Type: "array",
Items: &parser.Schema{Type: "string", Format: "uuid"},
}),
),
)
Multiple Content Types:
// OAS 3.x: Multiple content types for request body and responses
spec.AddOperation(http.MethodPost, "/users",
builder.WithRequestBodyContentTypes(
[]string{"application/json", "application/xml"},
User{},
),
builder.WithResponseContentTypes(http.StatusOK,
[]string{"application/json", "application/xml"},
User{},
),
)
📚 Deep Dive: For comprehensive examples and advanced patterns, see the Builder Deep Dive.
Overlay Package
The overlay package applies OpenAPI Overlay Specification v1.0.0 transformations to OpenAPI documents using JSONPath targeting.
Basic Usage:
import "github.com/erraggy/oastools/overlay"
// Apply overlay with functional options
result, err := overlay.ApplyWithOptions(
overlay.WithSpecFilePath("openapi.yaml"),
overlay.WithOverlayFilePath("changes.yaml"),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Applied %d actions\n", result.ActionsApplied)
Creating Overlays Programmatically:
// Create an overlay to update API metadata
o := &overlay.Overlay{
Version: "1.0.0",
Info: overlay.Info{
Title: "Update API Title",
Version: "1.0.0",
},
Actions: []overlay.Action{
{
Target: "$.info",
Update: map[string]any{
"title": "Production API",
"x-environment": "production",
},
},
{
Target: "$.paths[?@.x-internal==true]",
Remove: true,
},
},
}
result, err := overlay.ApplyWithOptions(
overlay.WithSpecFilePath("openapi.yaml"),
overlay.WithOverlayParsed(o),
)
Dry-Run Mode (Preview Changes):
// Preview changes without applying
dryResult, err := overlay.DryRunWithOptions(
overlay.WithSpecFilePath("openapi.yaml"),
overlay.WithOverlayFilePath("changes.yaml"),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Would apply: %d actions\n", dryResult.WouldApply)
fmt.Printf("Would skip: %d actions\n", dryResult.WouldSkip)
for _, change := range dryResult.Changes {
fmt.Printf(" - %s %d nodes at %s\n", change.Operation, change.MatchCount, change.Target)
}
Validating Overlay Documents:
o, err := overlay.ParseOverlayFile("overlay.yaml")
if err != nil {
log.Fatal(err)
}
errs := overlay.Validate(o)
if len(errs) > 0 {
for _, err := range errs {
fmt.Printf("Validation error: %s\n", err.Message)
}
}
Advanced JSONPath Targeting:
// The overlay package supports various JSONPath expressions:
// Recursive descent - find all descriptions at any depth
{Target: "$..description", Update: "Updated description"}
// Compound filters with AND
{Target: "$.paths[?@.deprecated==true && @.x-internal==true]", Remove: true}
// Compound filters with OR
{Target: "$.paths[?@.deprecated==true || @.x-obsolete==true]", Remove: true}
// Wildcard selectors
{Target: "$.paths.*.get", Update: map[string]any{"x-cached": true}}
Walker Package
The walker package provides document traversal with typed handlers for OpenAPI specifications.
Basic Traversal:
import (
"github.com/erraggy/oastools/parser"
"github.com/erraggy/oastools/walker"
)
// Parse document once
result, _ := parser.ParseWithOptions(parser.WithFilePath("openapi.yaml"))
// Walk with handlers - WalkContext provides method, path template, etc.
var operationCount int
err := walker.Walk(result,
walker.WithOperationHandler(func(wc *walker.WalkContext, op *parser.Operation) walker.Action {
// wc.Method contains HTTP method (get, post, etc.)
// wc.PathTemplate contains the URL path template
operationCount++
return walker.Continue
}),
)
Flow Control:
// Skip internal paths - WalkContext.PathTemplate available in path handlers
walker.Walk(result,
walker.WithPathHandler(func(wc *walker.WalkContext, pi *parser.PathItem) walker.Action {
if strings.HasPrefix(wc.PathTemplate, "/internal") {
return walker.SkipChildren
}
return walker.Continue
}),
)
// Stop on first match - WalkContext.JSONPath shows location in document
var found *parser.Schema
walker.Walk(result,
walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
if schema.Title == "Target" {
found = schema
return walker.Stop
}
return walker.Continue
}),
)
Mutation Support:
// Handlers receive pointers for in-place modification
walker.Walk(result,
walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
if schema.Extra == nil {
schema.Extra = make(map[string]any)
}
schema.Extra["x-processed"] = true
return walker.Continue
}),
)
Deep Dive: For comprehensive examples including cycle detection, depth limiting, and all handler types, see the Walker Deep Dive.
Advanced Features:
The walker package also supports:
- Parent Tracking: Access ancestor nodes with
WithParentTracking()and helper methods likeParentSchema(),ParentOperation() - Post-Visit Handlers: Process nodes after their children with
WithSchemaPostHandler(),WithOperationPostHandler(), etc. - Reference Tracking: Track
$refvalues withWithRefHandler()for reference analysis. UseWithMapRefTracking()to also detect refs inmap[string]anystructures within polymorphic schema fields
Advanced Patterns
Parse-Once Pattern
For workflows that process the same document multiple times, parse once and reuse the result:
// Parse once
parseResult, err := parser.ParseWithOptions(
parser.WithFilePath("openapi.yaml"),
parser.WithValidateStructure(true),
)
if err != nil {
log.Fatal(err)
}
// Validate (30x faster than validator.Validate)
valResult, _ := validator.ValidateWithOptions(
validator.WithParsed(*parseResult),
validator.WithIncludeWarnings(true),
)
// Convert (9x faster than converter.Convert)
convResult, _ := converter.ConvertWithOptions(
converter.WithParsed(*parseResult),
converter.WithTargetVersion("3.0.3"),
)
// Diff against another parsed document (81x faster than differ.Diff)
targetResult, _ := parser.ParseWithOptions(parser.WithFilePath("api-v2.yaml"))
diffResult, _ := differ.DiffWithOptions(
differ.WithSourceParsed(*parseResult),
differ.WithTargetParsed(*targetResult),
)
Package Chaining
After converting or joining documents, use ToParseResult() to chain the output with other packages:
Converter to Validator/Joiner/Differ:
// Convert OAS 2.0 to OAS 3.1
convResult, err := converter.ConvertWithOptions(
converter.WithFilePath("swagger.yaml"),
converter.WithTargetVersion("3.1.0"),
)
if err != nil {
log.Fatal(err)
}
// Chain to validator
v := validator.New()
valResult, _ := v.ValidateParsed(*convResult.ToParseResult())
// Or chain to joiner
j := joiner.New(joiner.DefaultConfig())
joinResult, _ := j.JoinParsed([]parser.ParseResult{
*convResult.ToParseResult(),
otherSpec,
})
// Or chain to differ
diffResult, _ := differ.DiffWithOptions(
differ.WithSourceParsed(baseSpec),
differ.WithTargetParsed(*convResult.ToParseResult()),
)
Joiner to Validator/Converter/Differ:
// Join multiple documents
joinResult, err := joiner.JoinWithOptions(
joiner.WithFilePaths([]string{"users-api.yaml", "orders-api.yaml"}),
)
if err != nil {
log.Fatal(err)
}
// Chain to validator
v := validator.New()
valResult, _ := v.ValidateParsed(*joinResult.ToParseResult())
// Or chain to converter
c := converter.New()
convResult, _ := c.ConvertParsed(*joinResult.ToParseResult(), "3.1.0")
// Or chain to fixer
fixResult, _ := fixer.FixWithOptions(
fixer.WithParsed(*joinResult.ToParseResult()),
)
Fixer to Validator/Converter/Joiner:
// Fix document
fixResult, err := fixer.FixWithOptions(
fixer.WithFilePath("openapi.yaml"),
fixer.WithInferTypes(true),
)
if err != nil {
log.Fatal(err)
}
// Chain to validator
v := validator.New()
valResult, _ := v.ValidateParsed(*fixResult.ToParseResult())
// Or chain to converter
c := converter.New()
convResult, _ := c.ConvertParsed(*fixResult.ToParseResult(), "3.1.0")
// Or chain to joiner with other specs
j := joiner.New(joiner.DefaultConfig())
joinResult, _ := j.JoinParsed([]parser.ParseResult{
*fixResult.ToParseResult(),
otherSpec,
})
Tip: When chaining multiple fixer passes, use
WithMutableInput(true)on subsequent passes to skip the defensive copy. EachToParseResult()output is a fresh document you already own, so in-place mutation is safe and avoids unnecessary allocations.
Overlay to Validator/Converter:
// Apply overlay transformations
applyResult, err := overlay.ApplyWithOptions(
overlay.WithSpecFilePath("openapi.yaml"),
overlay.WithOverlayFilePath("production.yaml"),
)
if err != nil {
log.Fatal(err)
}
// Chain to validator
v := validator.New()
valResult, _ := v.ValidateParsed(*applyResult.ToParseResult())
// Or chain to converter
c := converter.New()
convResult, _ := c.ConvertParsed(*applyResult.ToParseResult(), "3.1.0")
Validator to Converter/Differ (pass-through document):
// Validate document
valResult, err := validator.ValidateWithOptions(
validator.WithFilePath("openapi.yaml"),
validator.WithIncludeWarnings(true),
)
if err != nil {
log.Fatal(err)
}
if valResult.Valid {
// Chain validated document to converter
c := converter.New()
convResult, _ := c.ConvertParsed(*valResult.ToParseResult(), "3.1.0")
}
Differ to Validator/Converter (target document):
// Compare API versions
diffResult, err := differ.DiffWithOptions(
differ.WithSourceFilePath("api-v1.yaml"),
differ.WithTargetFilePath("api-v2.yaml"),
differ.WithMode(differ.ModeBreaking),
)
if err != nil {
log.Fatal(err)
}
// Report breaking changes, then continue with target (new version)
if diffResult.HasBreakingChanges {
fmt.Printf("⚠️ %d breaking changes\n", diffResult.BreakingCount)
}
// Chain the target (v2) to validator
v := validator.New()
valResult, _ := v.ValidateParsed(*diffResult.ToParseResult())
// ToParseResult() returns the TARGET document, enabling pipelines like:
// diff(v1, v2) → validate(v2) → convert(v2) → publish
Source Naming for Pre-Parsed Documents:
When joining pre-parsed documents, set meaningful SourcePath values for better collision reports:
// Set meaningful names when parsing from bytes/readers
result, _ := parser.ParseWithOptions(
parser.WithBytes(specData),
parser.WithSourceName("users-api"),
)
// Or set SourcePath after parsing
result.SourcePath = "users-api"
// The joiner emits info-level warnings for documents with generic source names
// to help identify when source naming is needed for clear collision reports
Error Handling
Handling Parse Errors:
result, err := parser.ParseWithOptions(
parser.WithFilePath("openapi.yaml"),
)
if err != nil {
// File not found, network error, or YAML/JSON syntax error
log.Fatalf("Failed to parse: %v", err)
}
if len(result.Errors) > 0 {
// Document parsed but has structural errors
fmt.Printf("Document has %d validation errors:\n", len(result.Errors))
for _, e := range result.Errors {
fmt.Printf(" - %s\n", e)
}
}
Handling Conversion Errors:
result, err := converter.ConvertWithOptions(
converter.WithFilePath("openapi.yaml"),
converter.WithTargetVersion("2.0"),
)
if err != nil {
// Parse error or unsupported conversion
log.Fatalf("Conversion failed: %v", err)
}
// Check for critical issues (features that couldn't be converted)
for _, issue := range result.Issues {
if issue.Severity == converter.SeverityCritical {
fmt.Printf("CRITICAL: %s\n", issue.Message)
// Decide whether to proceed or abort based on your requirements
}
}
Handling Join Errors:
result, err := joiner.JoinWithOptions(
joiner.WithFilePaths([]string{"base.yaml", "ext.yaml"}),
joiner.WithConfig(joiner.DefaultConfig()),
)
if err != nil {
switch {
case strings.Contains(err.Error(), "collision"):
fmt.Println("Path or schema collision detected")
fmt.Println("Use a different collision strategy or resolve conflicts manually")
case strings.Contains(err.Error(), "version mismatch"):
fmt.Println("Cannot join OAS 2.0 with OAS 3.x documents")
default:
log.Fatalf("Join failed: %v", err)
}
}
Working with Different OAS Versions
Detecting OAS Version:
result, _ := parser.ParseWithOptions(
parser.WithFilePath("spec.yaml"),
)
fmt.Printf("Version: %s\n", result.Version) // e.g., "3.0.3"
fmt.Printf("OAS Version: %d\n", result.OASVersion) // 2 or 3
switch doc := result.Document.(type) {
case *parser.OAS2Document:
// Swagger 2.0 specific handling
fmt.Printf("Host: %s\n", doc.Host)
fmt.Printf("BasePath: %s\n", doc.BasePath)
case *parser.OAS3Document:
// OpenAPI 3.x specific handling
for _, server := range doc.Servers {
fmt.Printf("Server: %s\n", server.URL)
}
}
Version-Specific Validation:
result, _ := validator.ValidateWithOptions(
validator.WithFilePath("spec.yaml"),
validator.WithIncludeWarnings(true),
)
// Validation automatically applies version-specific rules
// OAS 2.0: validates against Swagger 2.0 specification
// OAS 3.x: validates against OpenAPI 3.x specification
HTTP Client Configuration
The parser supports custom HTTP clients for advanced use cases:
Custom Timeout:
client := &http.Client{Timeout: 120 * time.Second}
result, err := parser.ParseWithOptions(
parser.WithFilePath("https://api.example.com/openapi.yaml"),
parser.WithHTTPClient(client),
)
Corporate Proxy:
proxyURL, _ := url.Parse("http://proxy.corp.internal:8080")
client := &http.Client{
Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
}
result, err := parser.ParseWithOptions(
parser.WithFilePath("https://internal-api.corp/spec.yaml"),
parser.WithHTTPClient(client),
)
Authentication via Custom Transport:
type authTransport struct {
token string
base http.RoundTripper
}
func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("Authorization", "Bearer "+t.token)
return t.base.RoundTrip(req)
}
client := &http.Client{
Transport: &authTransport{token: "secret", base: http.DefaultTransport},
}
result, err := parser.ParseWithOptions(
parser.WithFilePath("https://api.example.com/openapi.yaml"),
parser.WithHTTPClient(client),
)
Connection Pooling:
Reusing an HTTP client across multiple parse operations improves performance through connection pooling:
// Create once, reuse many times
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
},
}
// Use for multiple parses
result1, _ := parser.ParseWithOptions(parser.WithFilePath(url1), parser.WithHTTPClient(client))
result2, _ := parser.ParseWithOptions(parser.WithFilePath(url2), parser.WithHTTPClient(client))
InsecureSkipVerify Interaction:
When a custom client is provided, the InsecureSkipVerify option is ignored. Configure TLS on your client's transport instead:
import (
"crypto/tls"
"net/http"
)
// Instead of WithInsecureSkipVerify(true), do:
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
Troubleshooting
Common Issues
"missing required field 'openapi' or 'swagger'"
The document doesn't specify a version. Add either:
openapi: "3.0.0"(or another 3.x version) for OpenAPI 3.xswagger: "2.0"for Swagger 2.0
"$ref resolution failed: access denied"
External references are restricted to the base directory and subdirectories. Ensure referenced files are within the allowed path.
"cannot join OAS 2.0 with OAS 3.x documents"
All documents in a join operation must be the same major version. Convert documents to a common version first.
"collision at path '/users'"
Two documents define the same path. Choose a collision strategy:
accept-left: Keep the first definitionaccept-right: Keep the last definitionfail: Abort the operationfail-on-paths: Allow schema collisions but fail on path collisions
Performance Tips
- Use the Parse-Once pattern for workflows that process the same document multiple times
- Disable reference resolution when not needed:
parser.WithResolveRefs(false) - Disable validation during parsing if you'll validate separately:
parser.WithValidateStructure(false) - Reuse instances (Parser, Validator, Converter, Joiner, Differ) for processing multiple files
Getting Help
- API Documentation: pkg.go.dev/github.com/erraggy/oastools
- GitHub Issues: https://github.com/erraggy/oastools/issues
- Breaking Change Semantics: See breaking-changes.md
- Performance Details: See benchmarks.md