The OpenAPI linter validates OpenAPI specifications for style, consistency, and best practices beyond basic spec validation.
# Lint an OpenAPI specification
openapi spec lint api.yaml
# Output as JSON
openapi spec lint --format json api.yaml
# Disable specific rules
openapi spec lint --disable semantic-path-params api.yamlimport (
"context"
"fmt"
"os"
"github.com/speakeasy-api/openapi/linter"
"github.com/speakeasy-api/openapi/openapi"
openapiLinter "github.com/speakeasy-api/openapi/openapi/linter"
)
func main() {
ctx := context.Background()
// Load your OpenAPI document
f, _ := os.Open("api.yaml")
doc, validationErrors, _ := openapi.Unmarshal(ctx, f)
// Create linter with default configuration
config := linter.NewConfig()
lint := openapiLinter.NewLinter(config)
// Run linting
output, _ := lint.Lint(ctx, linter.NewDocumentInfo(doc, "api.yaml"), validationErrors, nil)
// Print results
fmt.Println(output.FormatText())
}The linter provides built-in rulesets that group related rules together:
| Ruleset | Description |
|---|---|
all |
All available rules (default) |
recommended |
Balanced ruleset for most APIs - includes semantic rules, essential style rules, and basic security rules |
security |
Comprehensive OWASP security rules for APIs that need strict security validation |
The recommended ruleset is a curated set of rules suitable for most APIs. It includes:
- Semantic rules - catch real bugs like missing path parameters, invalid operation IDs, ambiguous paths
- Essential style rules - info description, success responses, no trailing slashes, valid server URLs
- Basic security rules - no HTTP basic auth, no API keys in URLs, no credentials in URLs, HTTPS servers
extends: recommendedThe security ruleset includes all OWASP security rules:
extends: securityYou can extend multiple rulesets:
extends:
- recommended
- securityRules can be configured via YAML configuration file or command-line flags.
extends: recommended
rules:
- id: semantic-path-params
severity: error
- id: validation-required-field
match: ".*info\\.title is required.*"
disabled: true
# Enable custom rules (see Custom Rules section below)
custom_rules:
paths:
- ./rules/*.tsBy default, the CLI loads the config from ~/.openapi/lint.yaml unless --config is provided.
Write custom linting rules in TypeScript or JavaScript that run alongside the built-in rules.
- Install the types package in your rules directory:
npm install @speakeasy-api/openapi-linter-types- Create a rule file (e.g.,
rules/require-summary.ts):
import { Rule, registerRule, createValidationError } from '@speakeasy-api/openapi-linter-types';
import type { Context, DocumentInfo, RuleConfig, ValidationError } from '@speakeasy-api/openapi-linter-types';
class RequireOperationSummary extends Rule {
id(): string { return 'custom-require-operation-summary'; }
category(): string { return 'style'; }
description(): string { return 'All operations must have a summary.'; }
summary(): string { return 'Operations must have summary'; }
run(ctx: Context, docInfo: DocumentInfo, config: RuleConfig): ValidationError[] {
const errors: ValidationError[] = [];
for (const opNode of docInfo.index.operations) {
const op = opNode.node;
if (!op.getSummary()) {
errors.push(createValidationError(
config.getSeverity(this.defaultSeverity()),
this.id(),
`Operation "${op.getOperationID() || 'unnamed'}" is missing a summary`,
op.getRootNode()
));
}
}
return errors;
}
}
registerRule(new RequireOperationSummary());- Configure the linter (
lint.yaml):
extends: recommended
custom_rules:
paths:
- ./rules/*.ts
rules:
- id: custom-require-operation-summary
severity: errorExtend the Rule base class and implement the required methods:
| Method | Required | Description |
|---|---|---|
id() |
Yes | Unique identifier (prefix with custom-) |
category() |
Yes | Category for grouping (style, security, semantic, etc.) |
description() |
Yes | Full description of what the rule checks |
summary() |
Yes | Short summary for display |
run(ctx, docInfo, config) |
Yes | Main logic returning validation errors |
link() |
No | URL to rule documentation |
defaultSeverity() |
No | Default: 'warning'. Options: 'error', 'warning', 'hint' |
versions() |
No | OpenAPI versions this rule applies to (e.g., ['3.0', '3.1']) |
The DocumentInfo object provides access to the parsed OpenAPI document and pre-computed indices:
run(ctx: Context, docInfo: DocumentInfo, config: RuleConfig): ValidationError[] {
// Access the document root
const doc = docInfo.document;
const info = doc.getInfo();
// Access file location
const location = docInfo.location;
// Use the pre-computed index for efficient iteration
const index = docInfo.index;
// All operations in the document
for (const opNode of index.operations) {
const operation = opNode.node;
const path = opNode.locations.path;
const method = opNode.locations.method;
}
// All component schemas
for (const schemaNode of index.componentSchemas) {
const schema = schemaNode.node;
const name = schemaNode.locations.name;
}
// All inline schemas
for (const schemaNode of index.inlineSchemas) { ... }
// All parameters (inline + component)
for (const paramNode of index.parameters) { ... }
// All request bodies
for (const reqBodyNode of index.requestBodies) { ... }
// All responses
for (const responseNode of index.responses) { ... }
// All headers
for (const headerNode of index.headers) { ... }
// All security schemes
for (const secSchemeNode of index.securitySchemes) { ... }
// ... and many more collections
}Use createValidationError() to create properly formatted errors:
import { createValidationError } from '@speakeasy-api/openapi-linter-types';
// Basic error
errors.push(createValidationError(
'warning', // severity: 'error' | 'warning' | 'hint'
'custom-my-rule', // rule ID
'Description of the issue', // message
node.getRootNode() // YAML node for location
));
// Using config severity (respects user overrides)
errors.push(createValidationError(
config.getSeverity(this.defaultSeverity()),
this.id(),
'Description of the issue',
node.getRootNode()
));The console global is available for debugging:
console.log('Processing:', op.getOperationID());
console.warn('Missing recommended field');
console.error('Invalid configuration');Custom rules support all standard configuration options:
# Change severity
rules:
- id: custom-require-operation-summary
severity: error
# Disable a rule
rules:
- id: custom-require-operation-summary
disabled: true
# Filter by message pattern
rules:
- id: custom-require-operation-summary
match: ".*unnamed.*"
severity: hint
# Disable entire category
categories:
style:
enabled: falseEnable custom rules when using the linter as a Go library:
import (
"context"
// Import customrules package for side effects (registers the loader)
_ "github.com/speakeasy-api/openapi/openapi/linter/customrules"
"github.com/speakeasy-api/openapi/linter"
"github.com/speakeasy-api/openapi/openapi"
openapiLinter "github.com/speakeasy-api/openapi/openapi/linter"
)
func main() {
ctx := context.Background()
// Configure with custom rules
config := &linter.Config{
Extends: []string{"recommended"},
CustomRules: &linter.CustomRulesConfig{
Paths: []string{"./rules/*.ts"},
},
}
// Create linter (custom rules are automatically loaded)
lint, _ := openapiLinter.NewLinter(config)
// Load and lint document
f, _ := os.Open("api.yaml")
doc, validationErrs, _ := openapi.Unmarshal(ctx, f)
docInfo := linter.NewDocumentInfo(doc, "api.yaml")
output, _ := lint.Lint(ctx, docInfo, validationErrs, nil)
fmt.Println(output.FormatText())
}- Field and method names use lowercase JavaScript conventions (e.g.,
getSummary(), notGetSummary()) - All Go struct fields and methods are automatically exposed to JavaScript
- Arrays from the Index use JavaScript array methods (
.forEach(),.map(),.filter(), etc.) - Rules are transpiled with esbuild; source maps provide accurate error locations