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.
Important: WalkContext Pooling
WalkContext instances are reused via
sync.Poolfor performance. Handlers must not retain references to WalkContext after returning. If you need to preserve context information, copy the needed fields:// Wrong - retaining WalkContext reference var saved []*WalkContext WithSchemaHandler(func(wc *WalkContext, s *parser.Schema) Action { saved = append(saved, wc) // Don't do this! return Continue }) // Correct - copy needed fields type Info struct { JSONPath, Name string } var saved []Info WithSchemaHandler(func(wc *WalkContext, s *parser.Schema) Action { saved = append(saved, Info{wc.JSONPath, wc.Name}) return Continue })
| 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
ParseResultfrom 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 configuredmaxDepthlimit"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:
- The typed handler (
OAS2DocumentHandlerorOAS3DocumentHandler) is called first - If it returns
ContinueorSkipChildren, the genericDocumentHandleris called - 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
Reference Tracking
The walker provides optional $ref tracking to detect and process references during traversal without separate passes.
Enabling Reference Tracking
Use WithRefHandler to receive callbacks when references are encountered:
walker.Walk(result,
walker.WithRefHandler(func(wc *walker.WalkContext, ref *walker.RefInfo) walker.Action {
fmt.Printf("Found ref: %s at %s (type: %s)\n", ref.Ref, ref.SourcePath, ref.NodeType)
return walker.Continue
}),
)
RefInfo Structure
The RefInfo struct contains:
| Field | Description |
|---|---|
Ref |
The $ref value (e.g., #/components/schemas/User) |
SourcePath |
JSON path where the ref was encountered |
NodeType |
Type of node containing the ref |
Supported Node Types
References are tracked in:
| Node Type | Description |
|---|---|
schema |
Schema references |
parameter |
Parameter references |
response |
Response references |
requestBody |
Request body references |
header |
Header references |
pathItem |
Path item references |
link |
Link references |
example |
Example references |
securityScheme |
Security scheme references |
Use Cases
Collecting all references:
var refs []string
walker.Walk(result,
walker.WithRefHandler(func(wc *walker.WalkContext, ref *walker.RefInfo) walker.Action {
refs = append(refs, ref.Ref)
return walker.Continue
}),
)
Finding broken references:
walker.Walk(result,
walker.WithRefHandler(func(wc *walker.WalkContext, ref *walker.RefInfo) walker.Action {
if !isValidRef(ref.Ref) {
fmt.Printf("Broken ref at %s: %s\n", ref.SourcePath, ref.Ref)
}
return walker.Continue
}),
)
Stop on first external reference:
var hasExternal bool
walker.Walk(result,
walker.WithRefHandler(func(wc *walker.WalkContext, ref *walker.RefInfo) walker.Action {
if strings.HasPrefix(ref.Ref, "http") {
hasExternal = true
return walker.Stop
}
return walker.Continue
}),
)
WithRefTracking Option
WithRefTracking() enables internal reference tracking for statistics and debugging purposes, but it does not populate CurrentRef in node handlers. The CurrentRef field is only set when you register a RefHandler via WithRefHandler().
To check for references in node handlers, examine the node's Ref field directly:
walker.Walk(result,
walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
if schema.Ref != "" {
// This schema is a $ref - check schema.Ref directly
fmt.Printf("Schema ref at %s: %s\n", wc.JSONPath, schema.Ref)
}
return walker.Continue
}),
)
If you need the dedicated RefInfo structure with NodeType classification, use WithRefHandler() instead:
walker.Walk(result,
walker.WithRefHandler(func(wc *walker.WalkContext, ref *walker.RefInfo) walker.Action {
// RefInfo provides: Ref, SourcePath, NodeType
fmt.Printf("Ref %s at %s (type: %s)\n", ref.Ref, ref.SourcePath, ref.NodeType)
return walker.Continue
}),
)
Map Reference Tracking
Some polymorphic schema fields (Items, AdditionalItems, AdditionalProperties, UnevaluatedItems, UnevaluatedProperties) can contain either *parser.Schema or map[string]any. When a document is parsed with certain configurations or when schemas aren't fully resolved, these fields may contain raw maps with $ref values that the standard ref tracking wouldn't detect.
Use WithMapRefTracking() to enable detection of $ref values stored in these map structures:
walker.Walk(result,
walker.WithMapRefTracking(),
walker.WithRefHandler(func(wc *walker.WalkContext, ref *walker.RefInfo) walker.Action {
fmt.Printf("Found ref: %s at %s\n", ref.Ref, ref.SourcePath)
return walker.Continue
}),
)
Key behaviors:
WithMapRefTracking()implicitly enables standard ref tracking- The walker checks for
$refkeys inmap[string]anyvalues in polymorphic fields - Empty strings and non-string
$refvalues are ignored - Map-stored refs receive
RefNodeSchemaas their node type
Affected fields:
| Field | Description |
|---|---|
Items |
Array items schema |
AdditionalItems |
Additional array items schema |
UnevaluatedItems |
Unevaluated array items schema (OAS 3.1+) |
AdditionalProperties |
Additional object properties schema |
UnevaluatedProperties |
Unevaluated object properties schema (OAS 3.1+) |
Example with mixed schemas:
// Schema with both *Schema and map refs
doc := &parser.OAS3Document{
Components: &parser.Components{
Schemas: map[string]*parser.Schema{
"Container": {
Type: "object",
Properties: map[string]*parser.Schema{
"items": {
Type: "array",
Items: map[string]any{
"$ref": "#/components/schemas/Item",
},
},
"regular": {Ref: "#/components/schemas/Regular"},
},
},
},
},
}
// Wrap in ParseResult for walking
result := &parser.ParseResult{Document: doc, OASVersion: parser.OASVersion310}
// Both refs will be tracked with WithMapRefTracking()
walker.Walk(result,
walker.WithMapRefTracking(),
walker.WithRefHandler(func(wc *walker.WalkContext, ref *walker.RefInfo) walker.Action {
// Called for both the map-stored ref and the regular ref
return walker.Continue
}),
)
When to use:
- Parsing documents where polymorphic fields weren't fully resolved
- Working with documents from external sources that use map representations
- Comprehensive reference analysis that needs to catch all
$refvalues
Performance note: Map ref tracking adds a small overhead for type assertions on polymorphic fields. Only enable when needed.
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.
Built-in Collectors
The walker package provides convenience functions for common collection patterns, reducing boilerplate when you need to gather spec elements.
Five collectors are available: CollectSchemas, CollectOperations, CollectParameters, CollectResponses, and CollectSecuritySchemes.
When to Use Collectors vs Custom Handlers
Use built-in collectors when:
- You need all elements of one type in one pass
- You want ready-made lookup maps (by name, path, method, tag, status code, location)
- The standard collection fields meet your needs
Use custom handlers when:
- You need to filter during collection (e.g., only deprecated operations)
- You want to collect multiple node types in a single pass
- You need custom organization or aggregation logic
SchemaCollector
CollectSchemas walks a document and collects all schemas:
collector, err := walker.CollectSchemas(result)
if err != nil {
return err
}
// All schemas in traversal order
for _, info := range collector.All {
fmt.Printf("%s: %s\n", info.JSONPath, info.Schema.Type)
}
// Component schemas only
for _, info := range collector.Components {
fmt.Printf("Component %s at %s\n", info.Name, info.JSONPath)
}
// Inline schemas only (not in components)
for _, info := range collector.Inline {
fmt.Printf("Inline schema at %s\n", info.JSONPath)
}
// Lookup by JSON path
if schema, ok := collector.ByPath["$.components.schemas['Pet']"]; ok {
fmt.Printf("Pet: %v\n", schema.Schema.Type)
}
// Lookup by component name
if schema, ok := collector.ByName["Pet"]; ok {
fmt.Printf("Found Pet schema\n")
}
SchemaInfo fields:
| Field | Type | Description |
|---|---|---|
Schema |
*parser.Schema |
The collected schema |
Name |
string |
Component name (empty for inline schemas) |
JSONPath |
string |
Full JSON path to the schema |
IsComponent |
bool |
True when in components/definitions section |
OperationCollector
CollectOperations walks a document and collects all operations:
collector, err := walker.CollectOperations(result)
if err != nil {
return err
}
// All operations in traversal order
for _, info := range collector.All {
fmt.Printf("%s %s (%s)\n", info.Method, info.PathTemplate, info.Operation.OperationID)
}
// Group by path template
for path, ops := range collector.ByPath {
fmt.Printf("%s has %d operations\n", path, len(ops))
}
// Group by HTTP method
for method, ops := range collector.ByMethod {
fmt.Printf("%s: %d operations\n", method, len(ops))
}
// Group by tag
for tag, ops := range collector.ByTag {
fmt.Printf("Tag '%s': %d operations\n", tag, len(ops))
}
OperationInfo fields:
| Field | Type | Description |
|---|---|---|
Operation |
*parser.Operation |
The collected operation |
PathTemplate |
string |
URL path template (e.g., "/pets/{petId}") |
Method |
string |
HTTP method (e.g., "get", "post") |
JSONPath |
string |
Full JSON path to the operation |
ParameterCollector
CollectParameters walks a document and collects all parameters:
collector, err := walker.CollectParameters(result)
if err != nil {
return err
}
// All parameters in traversal order
for _, info := range collector.All {
fmt.Printf("%s (%s) at %s\n", info.Name, info.In, info.JSONPath)
}
// Group by location
for location, params := range collector.ByLocation {
fmt.Printf("%s: %d parameters\n", location, len(params))
}
// Group by path template
for path, params := range collector.ByPath {
fmt.Printf("%s has %d parameters\n", path, len(params))
}
ParameterInfo fields:
| Field | Type | Description |
|---|---|---|
Parameter |
*parser.Parameter |
The collected parameter |
Name |
string |
Parameter name |
In |
string |
Location: query, header, path, cookie |
JSONPath |
string |
Full JSON path to the parameter |
PathTemplate |
string |
Owning path template |
Method |
string |
Owning operation method (empty if path-level) |
IsComponent |
bool |
True when in components/parameters |
ResponseCollector
CollectResponses walks a document and collects all responses:
collector, err := walker.CollectResponses(result)
if err != nil {
return err
}
// All responses in traversal order
for _, info := range collector.All {
fmt.Printf("%s %s -> %s\n", info.Method, info.PathTemplate, info.StatusCode)
}
// Group by status code
for code, responses := range collector.ByStatusCode {
fmt.Printf("Status %s: %d responses\n", code, len(responses))
}
ResponseInfo fields:
| Field | Type | Description |
|---|---|---|
Response |
*parser.Response |
The collected response |
StatusCode |
string |
HTTP status code (e.g., "200", "default") |
JSONPath |
string |
Full JSON path to the response |
PathTemplate |
string |
Owning path template |
Method |
string |
Owning operation method |
IsComponent |
bool |
True when in components/responses |
SecuritySchemeCollector
CollectSecuritySchemes walks a document and collects all security schemes:
collector, err := walker.CollectSecuritySchemes(result)
if err != nil {
return err
}
// All security schemes
for _, info := range collector.All {
fmt.Printf("%s: type=%s\n", info.Name, info.SecurityScheme.Type)
}
// Lookup by name
if bearer, ok := collector.ByName["bearerAuth"]; ok {
fmt.Printf("Bearer scheme: %s\n", bearer.SecurityScheme.Scheme)
}
SecuritySchemeInfo fields:
| Field | Type | Description |
|---|---|---|
SecurityScheme |
*parser.SecurityScheme |
The collected security scheme |
Name |
string |
Security scheme name from components map key |
JSONPath |
string |
Full JSON path to the security scheme |
Example: API Coverage Report
func generateCoverageReport(result *parser.ParseResult) {
schemas, _ := walker.CollectSchemas(result)
ops, _ := walker.CollectOperations(result)
fmt.Printf("API Coverage Report\n")
fmt.Printf("==================\n\n")
fmt.Printf("Schemas: %d total (%d component, %d inline)\n",
len(schemas.All), len(schemas.Components), len(schemas.Inline))
fmt.Printf("Operations: %d total\n", len(ops.All))
fmt.Printf("\nOperations by Method:\n")
for method, methodOps := range ops.ByMethod {
fmt.Printf(" %s: %d\n", strings.ToUpper(method), len(methodOps))
}
fmt.Printf("\nOperations by Tag:\n")
for tag, tagOps := range ops.ByTag {
fmt.Printf(" %s: %d\n", tag, len(tagOps))
}
}
Parent Tracking
The walker supports optional parent/ancestor tracking, providing type-safe access to ancestor nodes during traversal. This is useful for context-aware processing where you need to know what contains the current node.
Enabling Parent Tracking
Use WithParentTracking() to enable ancestor tracking:
walker.Walk(result,
walker.WithParentTracking(),
walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
// wc.Parent is now populated
return walker.Continue
}),
)
Note: Parent tracking is disabled by default to avoid overhead when not needed. Enable it only when you need ancestor access.
ParentInfo Structure
The wc.Parent field provides a linked list of ancestors:
type ParentInfo struct {
Node any // The parent node (*parser.Schema, *parser.Operation, etc.)
JSONPath string // JSON path to this parent
Parent *ParentInfo // Grandparent (or nil at root)
}
Helper Methods
Type-safe helper methods make ancestor access convenient:
| Method | Returns | Description |
|---|---|---|
ParentSchema() |
(*parser.Schema, bool) |
Nearest ancestor schema |
ParentOperation() |
(*parser.Operation, bool) |
Nearest ancestor operation |
ParentPathItem() |
(*parser.PathItem, bool) |
Nearest ancestor path item |
ParentResponse() |
(*parser.Response, bool) |
Nearest ancestor response |
ParentRequestBody() |
(*parser.RequestBody, bool) |
Nearest ancestor request body |
Ancestors() |
[]*ParentInfo |
All ancestors (parent to root) |
Depth() |
int |
Number of ancestors |
Use Cases
1. Determining schema context:
walker.Walk(result,
walker.WithParentTracking(),
walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
// Is this schema in a request or response?
if _, ok := wc.ParentRequestBody(); ok {
fmt.Printf("Request schema: %s\n", wc.JSONPath)
} else if _, ok := wc.ParentResponse(); ok {
fmt.Printf("Response schema: %s\n", wc.JSONPath)
}
return walker.Continue
}),
)
2. Finding the containing operation:
walker.Walk(result,
walker.WithParentTracking(),
walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
if op, ok := wc.ParentOperation(); ok {
fmt.Printf("Schema in %s: %s\n", op.OperationID, wc.JSONPath)
}
return walker.Continue
}),
)
3. Detecting nested schemas:
walker.Walk(result,
walker.WithParentTracking(),
walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
if parentSchema, ok := wc.ParentSchema(); ok {
// This schema is nested within another schema
fmt.Printf("Nested in type: %v\n", parentSchema.Type)
} else if wc.IsComponent {
// This is a top-level component schema
fmt.Printf("Component schema: %s\n", wc.Name)
}
return walker.Continue
}),
)
4. Limiting depth based on ancestor count:
walker.Walk(result,
walker.WithParentTracking(),
walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
if wc.Depth() > 5 {
// Skip deeply nested schemas
return walker.SkipChildren
}
return walker.Continue
}),
)
Performance Considerations
Parent tracking adds overhead:
- ~15-20% increase in traversal time
- Additional allocations for ParentInfo structs
Only enable WithParentTracking() when you need ancestor access. If you only need the current node's context (JSONPath, Method, PathTemplate, etc.), the standard WalkContext fields are sufficient without parent tracking.
Post-Visit Hooks
Post-visit handlers fire after a node's children have been processed, enabling bottom-up processing patterns like aggregation and validation.
Available Post-Visit Handlers
| Option | Called After |
|---|---|
WithSchemaPostHandler |
Schema's children (properties, items, allOf, etc.) processed |
WithOperationPostHandler |
Operation's children (parameters, requestBody, responses, callbacks) processed |
WithPathItemPostHandler |
Path item's children (parameters, operations) processed |
WithResponsePostHandler |
Response's children (headers, content, links) processed |
WithRequestBodyPostHandler |
Request body's children (content) processed |
WithCallbackPostHandler |
Callback's children (path items) processed |
WithOAS2DocumentPostHandler |
OAS 2.0 document's children (all nodes) processed |
WithOAS3DocumentPostHandler |
OAS 3.x document's children (all nodes) processed |
When Post Handlers Are Called
Post handlers are called:
- AFTER all children are walked
- BEFORE the parent is popped (if parent tracking is enabled)
- NOT called if the pre-visit handler returned
SkipChildrenorStop
Execution Order
For nested schemas:
Pre-visit A (parent)
Pre-visit B (child)
Pre-visit C (grandchild)
Post-visit C
Post-visit B
Post-visit A
Use Cases
1. Counting child nodes:
propertyCounts := make(map[string]int)
walker.Walk(result,
walker.WithSchemaPostHandler(func(wc *walker.WalkContext, schema *parser.Schema) {
if wc.IsComponent && wc.Name != "" {
propertyCounts[wc.Name] = len(schema.Properties)
}
}),
)
2. Bottom-up validation:
var issues []string
walker.Walk(result,
walker.WithOperationPostHandler(func(wc *walker.WalkContext, op *parser.Operation) {
// Validate after all parameters and responses are processed
if op.OperationID == "" {
issues = append(issues, fmt.Sprintf("%s: missing operationId", wc.JSONPath))
}
}),
)
3. Aggregating statistics:
schemaStats := make(map[string]struct {
PropertyCount int
RequiredCount int
})
walker.Walk(result,
walker.WithSchemaPostHandler(func(wc *walker.WalkContext, schema *parser.Schema) {
if wc.IsComponent && wc.Name != "" && !strings.Contains(wc.JSONPath, ".properties") {
schemaStats[wc.Name] = struct {
PropertyCount int
RequiredCount int
}{
PropertyCount: len(schema.Properties),
RequiredCount: len(schema.Required),
}
}
}),
)
4. Building summary data after traversal:
var operationsByPath = make(map[string]int)
walker.Walk(result,
walker.WithPathItemPostHandler(func(wc *walker.WalkContext, pathItem *parser.PathItem) {
// Count operations in this path item
count := 0
if pathItem.Get != nil { count++ }
if pathItem.Post != nil { count++ }
if pathItem.Put != nil { count++ }
if pathItem.Delete != nil { count++ }
if pathItem.Patch != nil { count++ }
operationsByPath[wc.PathTemplate] = count
}),
)
Combining Pre and Post Handlers
You can use both pre and post handlers together:
walker.Walk(result,
walker.WithSchemaHandler(func(wc *walker.WalkContext, schema *parser.Schema) walker.Action {
// Pre-visit: mark schema as being processed
fmt.Printf("Entering %s\n", wc.JSONPath)
return walker.Continue
}),
walker.WithSchemaPostHandler(func(wc *walker.WalkContext, schema *parser.Schema) {
// Post-visit: mark schema as complete
fmt.Printf("Leaving %s\n", wc.JSONPath)
}),
)
Using Post Handlers Alone
Post handlers work without pre-handlers:
// Only register post handler
walker.Walk(result,
walker.WithSchemaPostHandler(func(wc *walker.WalkContext, schema *parser.Schema) {
// Called for every schema after its children are processed
}),
)
Document Post Handlers: Single-Walk Aggregation
Document post handlers (WithOAS2DocumentPostHandler, WithOAS3DocumentPostHandler) enable single-walk patterns where you collect information from child nodes and then modify the document root based on that collection.
Use case: Adding security definitions based on operation analysis
Without document post handlers, you'd need two walks:
// Old approach: Two walks required
needsOAuth2 := false
needsAPIKey := false
// Walk 1: Collect security requirements from operations
err := walker.Walk(result,
walker.WithOperationHandler(func(wc *walker.WalkContext, op *parser.Operation) walker.Action {
authType := getAuthType(op)
if authType == "oauth2" {
needsOAuth2 = true
} else if authType == "apiKey" {
needsAPIKey = true
}
return walker.Continue
}),
)
// Walk 2: Add security definitions based on collected info
err = walker.Walk(result,
walker.WithOAS3DocumentHandler(func(_ *walker.WalkContext, doc *parser.OAS3Document) walker.Action {
if needsOAuth2 {
doc.Components.SecuritySchemes["oauth2"] = &parser.SecurityScheme{...}
}
return walker.Continue
}),
)
With document post handlers (single walk):
// New approach: Single walk with document post handler
needsOAuth2 := false
needsAPIKey := false
scopes := make(map[string][]string)
err := walker.Walk(result,
walker.WithOperationHandler(func(wc *walker.WalkContext, op *parser.Operation) walker.Action {
authType := getAuthType(op)
if authType == "oauth2" {
needsOAuth2 = true
scopes[op.OperationID] = buildScopes(op)
} else if authType == "apiKey" {
needsAPIKey = true
}
return walker.Continue
}),
walker.WithOAS3DocumentPostHandler(func(_ *walker.WalkContext, doc *parser.OAS3Document) {
// Called AFTER all operations have been visited
if needsOAuth2 {
doc.Components.SecuritySchemes["oauth2"] = &parser.SecurityScheme{
Type: "oauth2",
Flows: buildOAuthFlows(scopes),
}
}
if needsAPIKey {
doc.Components.SecuritySchemes["api_key"] = &parser.SecurityScheme{
Type: "apiKey",
In: "header",
Name: "X-API-Key",
}
}
}),
)
This pattern is useful for:
- Adding security schemes based on operation requirements
- Generating documentation tags from operation tags
- Adding components discovered during traversal
- Validating document-wide constraints after seeing all nodes
Performance Considerations
Post handlers add minimal overhead since they reuse the existing WalkContext. The primary cost is the function call itself.