Skip to main content
Back to Blog
API Testing
2026-06-15

Spectral OpenAPI Linting Guide: Govern API Specs in CI (2026)

Learn Spectral OpenAPI linting: install the CLI, write custom rulesets, fail CI on spec violations, and enforce API governance across teams in 2026.

Spectral OpenAPI Linting Guide: Govern API Specs in CI (2026)

Spectral is an open-source JSON/YAML linter, maintained by Stoplight, that validates OpenAPI (and AsyncAPI) specifications against rulesets you control. You point it at a spec file, it walks the document using JSONPath expressions, and it reports every place your spec breaks a rule — a missing operationId, an undocumented error response, a path that ignores your naming convention. Run it locally to catch problems before commit, and wire it into CI so a non-conforming spec fails the pipeline. The result is consistent, governed APIs across every team without manual review.

This guide covers installing the CLI, the built-in OpenAPI ruleset, writing custom rules with given/then, severity levels, CI integration, and the errors you will actually hit.

Why lint an OpenAPI spec at all?

A schema validator only tells you whether your document is valid OpenAPI. It says nothing about whether it is a good API description. Spectral fills that gap. It is the difference between "this YAML parses" and "every operation has a summary, every 2xx has a schema, every path is kebab-case, and no one shipped a TODO description."

For organizations with many services, the spec is the contract. If specs drift in style and completeness, your generated SDKs, docs portals, and mock servers drift with them. Linting in CI turns governance from a wiki page nobody reads into an automated gate. It is the same shift-left logic behind running unit tests on every push — see our QA skills directory for related linting and contract-testing tools.

Installing Spectral

Spectral ships as an npm package with a CLI. The recommended way to run it in a project or CI is as a dev dependency so the version is pinned in package.json:

npm install --save-dev @stoplight/spectral-cli

For ad-hoc local runs you can use it without installing:

npx @stoplight/spectral-cli lint openapi.yaml

A standalone binary and a Docker image (stoplight/spectral) are also published if you do not want Node in your CI image:

docker run --rm -v "$PWD":/work stoplight/spectral \
  lint /work/openapi.yaml

Verify the install:

npx spectral --version

Your first lint run

Create a ruleset file named .spectral.yaml at the repo root. The fastest start is to extend Spectral's built-in OpenAPI ruleset, which bundles dozens of best-practice rules:

# .spectral.yaml
extends: ['spectral:oas']

spectral:oas is the OpenAPI ruleset (oas = OpenAPI Specification). There is also spectral:asyncapi for event-driven specs. Now lint a file:

npx spectral lint openapi.yaml

Typical output looks like this:

openapi.yaml
  12:7   warning  operation-operationId    Operation must have "operationId".          paths./users.get
  40:11  error    oas3-schema              "responses" property must have required ... paths./users.post.responses
  58:9   warning  operation-description    Operation "description" must be present...   paths./orders.get

✖ 3 problems (1 error, 2 warnings, 0 infos, 0 hints)

Each line gives the location (line:column), severity, the rule name, the message, and the JSONPath where it triggered. Spectral resolves $ref pointers before linting, so rules apply to the fully dereferenced document by default.

Severity levels and exit codes

Spectral has four severity levels: error, warn, info, and hint. By default the CLI exits with a non-zero code only when an error is found, which is exactly what you want for a CI gate. You can tighten this with --fail-severity:

# Fail the build on warnings too, not just errors
npx spectral lint openapi.yaml --fail-severity=warn

This is the single most important flag for governance. Set it to warn once your specs are clean, and any new warning blocks the merge.

Writing custom rules

The built-in ruleset is a starting point, not your house style. Custom rules are where Spectral earns its place. A rule has a given (a JSONPath selecting nodes), a then (a function applied to each matched node), and a severity.

Rule anatomy

extends: ['spectral:oas']
rules:
  # Every operation must have a summary
  operation-summary-required:
    description: All operations must include a summary.
    given: $.paths[*][get,post,put,patch,delete]
    severity: error
    then:
      field: summary
      function: truthy

given uses JSONPath: $.paths[*] selects every path item, and [get,post,...] narrows to HTTP methods. then.field: summary targets the summary property of each operation, and function: truthy asserts it exists and is non-empty.

Core functions

Spectral ships with built-in functions you compose into rules:

FunctionAsserts
truthyField is present and non-empty
falsyField is absent or falsy
definedField is present (even if empty)
undefinedField is absent
patternValue matches/doesn't match a regex
casingValue follows a casing style (camel, pascal, kebab, snake, etc.)
lengthString/array within min/max length
enumerationValue is one of an allowed set
schemaValue validates against a JSON Schema
alphabeticalArray/keys are sorted

A practical custom ruleset

Here is a ruleset that enforces several common governance rules at once:

extends: ['spectral:oas']
rules:
  # Paths must be kebab-case
  path-kebab-case:
    description: Paths must use kebab-case.
    given: $.paths[*]~
    severity: error
    then:
      function: pattern
      functionOptions:
        match: '^(\/[a-z0-9-]+(\{[a-zA-Z0-9]+\})?)+$'

  # Every operation needs a unique operationId
  operation-id-required:
    given: $.paths[*][get,post,put,patch,delete]
    severity: error
    then:
      field: operationId
      function: truthy

  # operationId should be camelCase
  operation-id-casing:
    given: $.paths[*][get,post,put,patch,delete].operationId
    severity: warn
    then:
      function: casing
      functionOptions:
        type: camel

  # Every endpoint must document a 4xx or 5xx response
  operation-has-error-response:
    description: Operations must define at least one error response.
    given: $.paths[*][get,post,put,patch,delete].responses
    severity: warn
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          patternProperties:
            '^(4|5)[0-9][0-9]$': true
          minProperties: 1

  # Ban TODO/FIXME in descriptions
  no-todo-in-description:
    given: $..description
    severity: error
    then:
      function: pattern
      functionOptions:
        notMatch: '(?i)(todo|fixme)'

The ~ suffix in $.paths[*]~ is JSONPath-Plus syntax meaning "the property key, not the value" — that is how you lint the path strings themselves rather than the path objects.

Custom JavaScript functions

When the built-ins are not enough, write a function in JavaScript. Reference it from a functions directory:

extends: ['spectral:oas']
functionsDir: './spectral-functions'
functions: [versionInPath]
rules:
  major-version-in-path:
    given: $.paths[*]~
    severity: error
    then:
      function: versionInPath
// spectral-functions/versionInPath.js
export default function (targetVal) {
  if (!/^\/v[0-9]+\//.test(targetVal)) {
    return [
      { message: `Path "${targetVal}" must start with a version segment like /v1/.` },
    ];
  }
}

A function returns an array of problem objects (each with a message), or nothing/undefined when the value passes.

Overrides and per-path exceptions

Real specs have exceptions. The overrides block lets you change severity or disable rules for specific files or JSONPath locations without polluting the spec with inline directives:

extends: ['spectral:oas']
overrides:
  # Legacy endpoints are grandfathered out of the casing rule
  - files:
      - 'openapi.yaml#/paths/~1legacy~1getStuff'
    rules:
      operation-id-casing: 'off'
  # The internal admin spec can be noisier
  - files:
      - 'specs/admin/**/*.yaml'
    rules:
      operation-description: warn

You can also disable a rule inline in the spec with an extension, but prefer overrides so exceptions live in one auditable place.

Running Spectral in CI

The whole point is the pipeline gate. Here is a GitHub Actions job that fails the build on any spec error:

# .github/workflows/api-lint.yml
name: API Lint
on:
  pull_request:
    paths: ['**/*.yaml', '**/*.yml', '**/*.json']
jobs:
  spectral:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - name: Lint OpenAPI spec
        run: npx spectral lint openapi.yaml --fail-severity=warn

For richer PR feedback, emit results in a machine-readable format and upload them. Spectral supports --format with stylish (default), json, junit, html, teamcity, pretty, and github-actions. The github-actions formatter produces inline annotations on the PR diff:

npx spectral lint openapi.yaml --format=github-actions --fail-severity=warn

For GitLab CI the job is just as short:

api-lint:
  image: stoplight/spectral:latest
  script:
    - spectral lint openapi.yaml --fail-severity=warn --format=junit > spectral.xml
  artifacts:
    reports:
      junit: spectral.xml

The JUnit output surfaces violations in the merge-request test report. If you want a deeper comparison of pipeline options for this kind of gate, see our CI tooling comparisons.

Linting multiple files

Spectral accepts globs, so a monorepo of specs lints in one command:

npx spectral lint "specs/**/*.{yml,yaml,json}" --fail-severity=warn

Each file is linted against the nearest applicable ruleset, or the one passed with --ruleset. Use --ruleset .spectral.yaml to force a specific governance ruleset regardless of where files sit.

Common errors and troubleshooting

"Cannot read ruleset" / no rules run. Spectral looks for .spectral.yaml, .spectral.yml, .spectral.json, or .spectral.js in the working directory. If your file is elsewhere, pass --ruleset path/to/ruleset.yaml. A common mistake is forgetting extends: ['spectral:oas'], which leaves you with zero built-in rules.

$ref resolution failures. By default Spectral resolves all references. A broken or circular $ref aborts linting. Use --ignore-unknown-format only for non-OpenAPI files; for genuinely external refs, ensure the referenced files are present in CI (check them out, do not rely on network fetches).

Rule matches nothing. This is almost always a given JSONPath bug. Test expressions interactively — Spectral uses JSONPath-Plus, so $..parameters[*] and filter syntax $.paths[*][get].parameters[?(@.in=='query')] are supported. Add --verbose to see which rules ran and how many results each produced.

Everything is a warning and CI passes anyway. You forgot --fail-severity. By default only error fails the build; set --fail-severity=warn once your spec is clean.

Casing function on the wrong node. function: casing operates on a string value. If you point it at an object you get no error and no result. For path keys remember the ~ operator.

A recommended rollout strategy

Adopt Spectral incrementally so you do not block every PR on day one:

  1. Start with extends: ['spectral:oas'] and --fail-severity=error. This catches genuinely invalid specs only.
  2. Add custom rules at severity: warn. The team sees them in CI output without being blocked.
  3. Fix the existing warnings spec-by-spec, using overrides to grandfather legacy paths.
  4. Flip the gate to --fail-severity=warn once the backlog is clear. New violations now block merges.
  5. Promote stable warnings to error over time.

This mirrors how teams roll out any new quality gate — start observe-only, then enforce. The broader practice of contract-first API development pairs well with linting; explore related guides on the QASkills blog.

Frequently Asked Questions

What is the difference between Spectral and a regular OpenAPI validator?

A validator checks whether your document is structurally valid against the OpenAPI schema — correct keywords, correct types. Spectral does that too (via spectral:oas) but goes further: it enforces style and completeness rules you define, like required summaries, naming conventions, and documented error responses. Validation answers "is this valid OpenAPI?"; Spectral answers "is this a good API description by our standards?"

How do I make Spectral fail my CI build on warnings?

Pass the --fail-severity flag. By default the CLI exits non-zero only on error-level results, so warnings are reported but do not break the build. Run spectral lint openapi.yaml --fail-severity=warn to treat warnings as failures. This is the standard way to enforce a governance ruleset once your specs are clean.

Can Spectral lint AsyncAPI and other JSON/YAML files, not just OpenAPI?

Yes. Spectral ships spectral:oas for OpenAPI and spectral:asyncapi for AsyncAPI, and you extend whichever fits. Because the engine is a generic JSON/YAML linter driven by JSONPath, you can also write rulesets for arbitrary config files, Kubernetes manifests, or any structured document — the OpenAPI knowledge lives entirely in the ruleset, not the engine.

How do I write a custom Spectral rule?

Define a rule with three parts: given (a JSONPath selecting the nodes to check), then (a built-in or custom function applied to each node, plus an optional field), and severity. For logic the built-in functions cannot express, set functionsDir and write a JavaScript function that returns an array of problem messages. Place all rules under the rules key in your .spectral.yaml.

Should Spectral run before or after schema validation in my pipeline?

Run Spectral as your single spec-quality gate; it performs structural validation via spectral:oas and your custom governance rules in one pass, so a separate validator step is usually redundant. Place the job early in the pipeline on every pull request that touches spec files, so contributors get fast feedback before code review rather than after merge.

Does Spectral resolve $ref references before linting?

Yes, by default Spectral fully dereferences the document, so rules apply to the resolved spec including content pulled in via $ref. This means a broken or unreachable reference will abort the lint, so ensure all referenced files are available in CI. If you need rules to inspect the raw, unresolved document instead, Spectral exposes a resolved/unresolved distinction in its programmatic API for advanced cases.

Spectral OpenAPI Linting Guide: Govern API Specs in CI (2026) | QASkills.sh