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
- Document root
- Info
- ExternalDocs (root level)
- Servers
- Paths โ PathItems โ Operations โ Parameters, RequestBody, Responses, Callbacks
- Webhooks (OAS 3.1+)
- Components (schemas, responses, parameters, requestBodies, headers, securitySchemes, links, callbacks, examples, pathItems)
- Tags
OAS 2.0 Documents
- Document root
- Info
- ExternalDocs (root level)
- Paths โ PathItems โ Operations โ Parameters, Responses
- Definitions (schemas)
- Parameters (reusable)
- Responses (reusable)
- SecurityDefinitions
- 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
ParseResultinstead of file paths - Minimal Allocations: Handler function calls have minimal overhead
- Deterministic Order: Map keys are sorted for consistent traversal
- Early Exit: Use
Stopto 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.Queryoperation (QUERY method)PathItem.AdditionalOperationsfor custom methodsComponents.MediaTypesfor 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.