Skip to content

Walker Package Deep Dive

The walker package provides a document traversal API for OpenAPI specifications, enabling single-pass traversal with typed handlers for analysis and mutation.

Overview

The walker visits all nodes in an OpenAPI document in a consistent order, calling registered handlers for each node type. This is useful for:

  • Analysis: Collecting statistics, finding patterns, validating custom rules
  • Transformation: Adding vendor extensions, modifying descriptions, normalizing formats
  • Code Generation: Gathering type information across the document

Core Concepts

Action-Based Flow Control

Handlers return an Action to control traversal:

type Action int

const (
    Continue     Action = iota  // Continue to children and siblings
    SkipChildren                // Skip children, continue to siblings
    Stop                        // Stop walking entirely
)

This provides cleaner flow control than error-based approaches.

Continue

Continue tells the walker to proceed normallyโ€”descend into children, then continue to siblings. This is the default behavior for most handlers.

// Count all schemas in the document
var schemaCount int
walker.Walk(result,
    walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
        schemaCount++
        return walker.Continue  // Visit nested schemas too
    }),
)

Use Continue when you want to: - Visit every matching node in the document - Collect comprehensive information (all operations, all schemas, etc.) - Apply transformations uniformly across the entire document

SkipChildren

SkipChildren tells the walker to skip the current node's descendants but continue to siblings. The walker moves horizontally rather than descending.

// Find schemas but don't descend into their nested properties
var topLevelSchemas []string
walker.Walk(result,
    walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
        // Only capture component-level schemas, not nested ones
        if wc.IsComponent && wc.Name != "" {
            topLevelSchemas = append(topLevelSchemas, wc.JSONPath)
        }
        return walker.SkipChildren  // Don't walk into properties/items/etc.
    }),
)

Common use cases for SkipChildren:

1. Skipping internal/private paths:

walker.Walk(result,
    walker.WithPathHandler(func(wc *walker.WalkContext, pi *parser.PathItem) walker.Action {
        if strings.HasPrefix(wc.PathTemplate, "/internal") ||
           strings.HasPrefix(wc.PathTemplate, "/_") {
            return walker.SkipChildren  // Don't process internal endpoints
        }
        return walker.Continue
    }),
)

2. Processing only top-level schemas (ignoring nested):

walker.Walk(result,
    walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
        // Process this schema...
        processSchema(schema)
        // But don't recurse into properties, items, allOf, etc.
        return walker.SkipChildren
    }),
)

3. Conditional depth limiting:

walker.Walk(result,
    walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
        depth := strings.Count(wc.JSONPath, ".properties")
        if depth >= 3 {
            return walker.SkipChildren  // Stop at 3 levels of nesting
        }
        return walker.Continue
    }),
)

4. Skipping deprecated operations:

walker.Walk(result,
    walker.WithOperationHandler(func(wc *walker.WalkContext, op *parser.Operation) walker.Action {
        if op.Deprecated {
            return walker.SkipChildren  // Skip parameters, responses of deprecated ops
        }
        return walker.Continue
    }),
)

Stop

Stop immediately terminates the entire walk. No more nodes are visitedโ€”the walker returns immediately.

// Find the first schema with a specific title
var targetSchema *parser.Schema
walker.Walk(result,
    walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
        if schema.Title == "UserProfile" {
            targetSchema = schema
            return walker.Stop  // Found it, no need to continue
        }
        return walker.Continue
    }),
)

Common use cases for Stop:

1. Search with early termination:

// Check if any operation uses a specific security scheme
var usesOAuth bool
walker.Walk(result,
    walker.WithOperationHandler(func(wc *walker.WalkContext, op *parser.Operation) walker.Action {
        for _, req := range op.Security {
            if _, ok := req["oauth2"]; ok {
                usesOAuth = true
                return walker.Stop  // Found one, that's enough
            }
        }
        return walker.Continue
    }),
)

2. Validation with fail-fast:

// Stop on first validation error
var firstError error
walker.Walk(result,
    walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
        if err := validateCustomRule(schema); err != nil {
            firstError = fmt.Errorf("%s: %w", wc.JSONPath, err)
            return walker.Stop  // Fail fast
        }
        return walker.Continue
    }),
)

3. Finding a specific node by path:

// Find operation at a specific path and method
var targetOp *parser.Operation
walker.Walk(result,
    walker.WithOperationHandler(func(wc *walker.WalkContext, op *parser.Operation) walker.Action {
        if wc.JSONPath == "$.paths['/users/{id}'].get" {
            targetOp = op
            return walker.Stop
        }
        return walker.Continue
    }),
)

4. Resource limits:

// Process at most N schemas
const maxSchemas = 1000
var processed int
walker.Walk(result,
    walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
        processed++
        if processed >= maxSchemas {
            return walker.Stop  // Resource limit reached
        }
        // Process schema...
        return walker.Continue
    }),
)

Combining Actions Across Handlers

Different handlers can return different actions to create sophisticated traversal patterns:

// Analyze public APIs only, stop if we find a critical issue
var criticalIssue error
walker.Walk(result,
    walker.WithPathHandler(func(wc *walker.WalkContext, pi *parser.PathItem) walker.Action {
        if strings.HasPrefix(wc.PathTemplate, "/internal") {
            return walker.SkipChildren  // Skip internal paths
        }
        return walker.Continue
    }),
    walker.WithOperationHandler(func(wc *walker.WalkContext, op *parser.Operation) walker.Action {
        if op.Deprecated {
            return walker.SkipChildren  // Skip deprecated operations
        }
        return walker.Continue
    }),
    walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
        if hasCriticalVulnerability(schema) {
            criticalIssue = fmt.Errorf("critical issue at %s", wc.JSONPath)
            return walker.Stop  // Halt everything
        }
        return walker.Continue
    }),
)

Handler Types

Each OAS node type has a corresponding handler type:

Handler Called For OAS Version
DocumentHandler Root document (any type) All
OAS2DocumentHandler OAS 2.0 documents only 2.0 only
OAS3DocumentHandler OAS 3.x documents only 3.x only
InfoHandler API metadata All
ServerHandler Server definitions 3.x only
TagHandler Tag definitions All
PathHandler Path entries All
PathItemHandler Path items All
OperationHandler Operations All
ParameterHandler Parameters All
RequestBodyHandler Request bodies 3.x only
ResponseHandler Responses All
SchemaHandler Schemas (including nested) All
SecuritySchemeHandler Security schemes All
HeaderHandler Headers All
MediaTypeHandler Media types 3.x only
LinkHandler Links 3.x only
CallbackHandler Callbacks 3.x only
ExampleHandler Examples All
ExternalDocsHandler External docs All
SchemaSkippedHandler Skipped schemas (depth/cycle) All

WalkContext

Every handler receives a *WalkContext as its first parameter, providing contextual information about the current node:

Field Description
JSONPath Full JSON path to the node (always populated)
PathTemplate URL path template when in $.paths scope
Method HTTP method when in operation scope (e.g., "get", "post")
StatusCode Status code when in response scope (e.g., "200", "default")
Name Map key for named items (headers, schemas, etc.)
IsComponent True when in components/definitions section

JSON Path Examples

$                                    # Document root
$.info                               # Info object
$.paths['/pets/{petId}']             # Path entry
$.paths['/pets'].get                 # Operation
$.paths['/pets'].get.parameters[0]   # Parameter
$.components.schemas['Pet']          # Schema
$.components.schemas['Pet'].properties['name']  # Nested schema

Accessing Context

walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
    if wc.IsComponent {
        fmt.Printf("Component schema: %s\n", wc.Name)
    } else if wc.InOperationScope() {
        fmt.Printf("Inline schema in %s %s operation\n", wc.Method, wc.PathTemplate)
    }
    return walker.Continue
})

Scope Helper Methods

The WalkContext provides helper methods to check the current scope:

wc.InPathsScope()     // true when PathTemplate is set
wc.InOperationScope() // true when Method is set
wc.InResponseScope()  // true when StatusCode is set

Cancellation Support

Pass a context.Context for cancellation and timeout support:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

walker.Walk(result,
    walker.WithContext(ctx),
    walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
        // Check if cancelled
        if wc.Context().Err() != nil {
            return walker.Stop
        }
        return walker.Continue
    }),
)

API Reference

Choosing an API: Walk vs WalkWithOptions

The walker package provides two complementary APIs:

API Best For Input Error Handling
Walk Pre-parsed documents *parser.ParseResult Handler registration never fails
WalkWithOptions File paths or parsed documents Via options Option functions can return errors

Use Walk when: - You already have a ParseResult from parsing - You're walking multiple documents with the same handlers - You want simpler handler registration (no error checking)

// Walk: Direct and simple
result, _ := parser.New().Parse("openapi.yaml")
walker.Walk(result,
    walker.WithSchemaHandler(handler),
    walker.WithMaxDepth(50),
)

Use WalkWithOptions when: - You want to parse and walk in a single call - You need error handling for configuration (e.g., invalid depth)

// WalkWithOptions: Parse and walk in one call
err := walker.WalkWithOptions(
    walker.WithFilePath("openapi.yaml"),
    walker.WithSchemaHandler(handler),
    walker.WithMaxSchemaDepth(50),  // Uses default (100) if not positive
)

Primary Functions

// Walk traverses a parsed document with registered handlers
func Walk(result *parser.ParseResult, opts ...Option) error

// WalkWithOptions provides functional options for input and configuration
func WalkWithOptions(opts ...Option) error

Walk Options

// Handler registration
WithDocumentHandler(fn DocumentHandler)
WithInfoHandler(fn InfoHandler)
WithServerHandler(fn ServerHandler)
WithTagHandler(fn TagHandler)
WithPathHandler(fn PathHandler)
WithPathItemHandler(fn PathItemHandler)
WithOperationHandler(fn OperationHandler)
WithParameterHandler(fn ParameterHandler)
WithRequestBodyHandler(fn RequestBodyHandler)
WithResponseHandler(fn ResponseHandler)
WithSchemaHandler(fn SchemaHandler)
WithSecuritySchemeHandler(fn SecuritySchemeHandler)
WithHeaderHandler(fn HeaderHandler)
WithMediaTypeHandler(fn MediaTypeHandler)
WithLinkHandler(fn LinkHandler)
WithCallbackHandler(fn CallbackHandler)
WithExampleHandler(fn ExampleHandler)
WithExternalDocsHandler(fn ExternalDocsHandler)
WithSchemaSkippedHandler(fn SchemaSkippedHandler)

// Configuration
WithMaxDepth(depth int)  // Default: 100

WalkWithOptions Input Options

WithFilePath(path string)               // Parse and walk a file
WithParsed(result *parser.ParseResult)  // Walk pre-parsed document
WithMaxSchemaDepth(depth int)           // Silently ignored if not positive (uses default 100)
WithUserContext(ctx context.Context)    // Context for cancellation

All handler options (e.g., WithSchemaHandler, WithOperationHandler) work directly with both Walk and WalkWithOptions.

Walk Order

OAS 3.x Documents

  1. Document root
  2. Info
  3. ExternalDocs (root level)
  4. Servers
  5. Paths โ†’ PathItems โ†’ Operations โ†’ Parameters, RequestBody, Responses, Callbacks
  6. Webhooks (OAS 3.1+)
  7. Components (schemas, responses, parameters, requestBodies, headers, securitySchemes, links, callbacks, examples, pathItems)
  8. Tags

OAS 2.0 Documents

  1. Document root
  2. Info
  3. ExternalDocs (root level)
  4. Paths โ†’ PathItems โ†’ Operations โ†’ Parameters, Responses
  5. Definitions (schemas)
  6. Parameters (reusable)
  7. Responses (reusable)
  8. SecurityDefinitions
  9. Tags

Schema Walking

The walker recursively visits all nested schemas:

  • properties, patternProperties, dependentSchemas, $defs (maps)
  • allOf, anyOf, oneOf, prefixItems (slices)
  • items, additionalProperties, additionalItems, unevaluatedItems, unevaluatedProperties (polymorphic)
  • not, contains, propertyNames, contentSchema, if, then, else (single)

Cycle Detection

The walker uses pointer-based cycle detection to prevent infinite loops in circular schema references. Visited schemas are tracked and skipped on subsequent encounters.

// Circular reference example
schema := &parser.Schema{Type: "object"}
schema.Properties = map[string]*parser.Schema{
    "self": schema,  // Points back to itself
}
// The walker will visit 'schema' once, then skip 'self'
// since it's already been visited

Depth Limiting

Use WithMaxDepth(n) to limit schema recursion depth (default: 100).

// Limit to 10 levels of nesting
walker.Walk(result,
    walker.WithSchemaHandler(handler),
    walker.WithMaxDepth(10),
)

Behavior: - The depth counter starts at 0 for component/definition schemas - Each nested schema (properties, items, allOf, etc.) increments the depth - When depth reaches the limit, nested schemas are skipped - The handler is not called for schemas beyond the depth limit

Schema Skipped Callbacks

Use WithSchemaSkippedHandler to receive notifications when schemas are skipped due to depth limits or cycle detection:

walker.Walk(result,
    walker.WithMaxDepth(10),
    walker.WithSchemaSkippedHandler(func(wc *walker.WalkContext, reason string, schema *parser.Schema) {
        switch reason {
        case "depth":
            fmt.Printf("Skipped due to depth limit: %s\n", wc.JSONPath)
        case "cycle":
            fmt.Printf("Skipped due to circular reference: %s\n", wc.JSONPath)
        }
    }),
)

Reason values: - "depth" - Schema exceeded the configured maxDepth limit - "cycle" - Schema was already visited (circular reference detected)

This is useful for: - Debugging: Understanding why certain schemas weren't processed - Logging: Recording when circular references are encountered - Validation: Detecting overly deep or circular schema structures

With WalkWithOptions:

walker.WalkWithOptions(
    walker.WithFilePath("openapi.yaml"),
    walker.WithMaxSchemaDepth(10),
    walker.WithSchemaSkippedHandler(func(wc *walker.WalkContext, reason string, schema *parser.Schema) {
        log.Printf("Schema skipped (%s): %s", reason, wc.JSONPath)
    }),
)

Usage Patterns

Mutation

Handlers receive pointers to the actual document nodes, allowing in-place modification:

walker.Walk(result,
    walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
        // Add vendor extension to all schemas
        if schema.Extra == nil {
            schema.Extra = make(map[string]any)
        }
        schema.Extra["x-visited"] = true
        return walker.Continue
    }),
)

Version-Specific Handling

For type-safe version-specific handling, use the typed document handlers:

walker.Walk(result,
    walker.WithOAS2DocumentHandler(func(wc *walker.WalkContext, doc *parser.OAS2Document) walker.Action {
        // Called only for OAS 2.0 documents - doc is already typed
        fmt.Printf("OAS 2.0: %s (host: %s)\n", doc.Info.Title, doc.Host)
        return walker.Continue
    }),
    walker.WithOAS3DocumentHandler(func(wc *walker.WalkContext, doc *parser.OAS3Document) walker.Action {
        // Called only for OAS 3.x documents - doc is already typed
        fmt.Printf("OAS 3.x: %s (servers: %d)\n", doc.Info.Title, len(doc.Servers))
        return walker.Continue
    }),
)

Handler Order: When both typed and generic handlers are registered: 1. The typed handler (OAS2DocumentHandler or OAS3DocumentHandler) is called first 2. If it returns Continue or SkipChildren, the generic DocumentHandler is called 3. If it returns Stop, the generic handler is skipped and the walk stops

Alternatively, use a type switch with the generic handler:

walker.Walk(result,
    walker.WithDocumentHandler(func(wc *walker.WalkContext, doc any) walker.Action {
        switch d := doc.(type) {
        case *parser.OAS2Document:
            fmt.Printf("OAS 2.0: %s\n", d.Info.Title)
        case *parser.OAS3Document:
            fmt.Printf("OAS 3.x: %s\n", d.Info.Title)
        }
        return walker.Continue
    }),
)

Multiple Handlers

Register multiple handlers to build up analysis in a single pass:

var (
    pathCount      int
    operationCount int
    schemaCount    int
)

walker.Walk(result,
    walker.WithPathHandler(func(wc *walker.WalkContext, pi *parser.PathItem) walker.Action {
        pathCount++
        return walker.Continue
    }),
    walker.WithOperationHandler(func(wc *walker.WalkContext, op *parser.Operation) walker.Action {
        operationCount++
        return walker.Continue
    }),
    walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
        schemaCount++
        return walker.Continue
    }),
)

Using WalkContext for Location-Aware Processing

The WalkContext enables location-aware processing using both structured fields and the JSON path:

walker.Walk(result,
    walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
        // Use structured fields for cleaner logic
        if wc.IsComponent {
            // Component schema - wc.Name has the schema name
            fmt.Printf("Component: %s\n", wc.Name)
        } else if wc.InOperationScope() {
            // Inline schema in an operation
            fmt.Printf("Inline in %s %s\n", wc.Method, wc.PathTemplate)
        }

        // Or use JSON path for more specific matching
        switch {
        case strings.HasPrefix(wc.JSONPath, "$.components.schemas"):
            // Component schema
        case strings.Contains(wc.JSONPath, ".requestBody"):
            // Request body schema
        case strings.Contains(wc.JSONPath, ".responses"):
            // Response schema
        }
        return walker.Continue
    }),
)

WalkWithOptions API

For parsing and walking in one call:

err := walker.WalkWithOptions(
    walker.WithFilePath("openapi.yaml"),
    walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
        fmt.Println(wc.JSONPath)
        return walker.Continue
    }),
)

Performance

  • Parse-Once Pattern: Pass pre-parsed ParseResult instead of file paths
  • Minimal Allocations: Handler function calls have minimal overhead
  • Deterministic Order: Map keys are sorted for consistent traversal
  • Early Exit: Use Stop to terminate as soon as you find what you need

Thread Safety

โš ๏ธ The Walker is NOT thread-safe. Each walk maintains internal state (visited schemas, stopped flag) that is not protected by locks.

Safe patterns:

// โœ… Sequential walks (same or different documents)
walker.Walk(result1, opts...)
walker.Walk(result2, opts...)

// โœ… Parallel walks with separate documents
var wg sync.WaitGroup
for _, doc := range documents {
    wg.Add(1)
    go func(d *parser.ParseResult) {
        defer wg.Done()
        walker.Walk(d, opts...)  // Each goroutine has its own walk state
    }(doc)
}
wg.Wait()

Unsafe patterns:

// โŒ Shared mutable state in handlers without synchronization
var count int  // Race condition!
walker.Walk(result,
    walker.WithSchemaHandler(func(wc *walker.WalkContext, s *parser.Schema) walker.Action {
        count++  // Not thread-safe
        return walker.Continue
    }),
)

// โœ… Use atomic operations or mutexes for shared state
var count atomic.Int64
walker.Walk(result,
    walker.WithSchemaHandler(func(wc *walker.WalkContext, s *parser.Schema) walker.Action {
        count.Add(1)  // Thread-safe
        return walker.Continue
    }),
)

Document mutation: If handlers modify the document, ensure the document is not shared across concurrent walks.

OAS 3.2 Support

The walker supports OAS 3.2 features:

  • PathItem.Query operation (QUERY method)
  • PathItem.AdditionalOperations for custom methods
  • Components.MediaTypes for reusable media types

Examples

The examples/walker/ directory contains runnable examples demonstrating walker patterns:

Example Category Description
api-statistics Analysis Collect API statistics in single pass
security-audit Validation Audit for security issues
vendor-extensions Mutation Add vendor extensions
public-api-filter Filtering Extract public API only
api-documentation Reporting Generate Markdown docs
reference-collector Integration Analyze schema references

Each example includes a README with detailed explanations and expected output.