To validate an OpenAPI spec, run it through three checks in order. First parse it, so YAML or JSON syntax errors surface. Then check the parsed document against the JSON Schema published for its OpenAPI version, which catches missing required fields and wrong types. Then run a linter with rules that look across the whole document, such as unique operationIds and path parameters that match the path template. Run all three on every pull request that touches the spec. Passing them proves the file is well formed. It does not prove the API is well designed or that the server returns what the file says, and the last section covers those gaps.
The layers of spec validation
| Layer | What it checks | Example failure | Typical tool |
|---|---|---|---|
| Syntax | The file is valid YAML or JSON | A tab used for indentation | Any YAML or JSON parser |
| Structure | The document matches the OpenAPI JSON Schema for its version | A response with no description | swagger-parser validate(), and the struct rule in Redocly or oas3-schema in Spectral |
| Cross-document rules | Rules that span several objects | Two operations with the same operationId | Redocly CLI or Spectral rulesets; swagger-parser for Swagger 2.0 only |
| Style and design | Your team's conventions | Paths in camelCase, no 4xx responses | Linter rules you configure, or the APIForge design score |
The first two layers are objective: the file either follows the specification or it does not. The third is mostly objective too, since rules like operationId uniqueness come straight from the specification text, but a JSON Schema cannot express them. The fourth is opinion, which is why linters let you turn rules off or change their severity. If you are still fixing parser messages, the common OpenAPI errors guide lists them with fixes; this guide is about setting up the checks.
A spec that passes structural validation with three bugs
Here is what the gap between the second and third layers looks like in practice. This OpenAPI 3.0.3 file has three mistakes. Both operations use the operationId getOrder. The path /v1/orders/{id} has no parameter declared for id. The example on the 200 response sets status to cancelled, which is not in the enum.
openapi: 3.0.3
info:
title: Orders API
version: 1.0.0
paths:
/v1/orders:
get:
operationId: getOrder
summary: List orders
responses:
"200":
description: A page of orders
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Order"
"400":
description: Bad query
/v1/orders/{id}:
get:
operationId: getOrder
summary: Get an order
responses:
"200":
description: The order
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
example:
id: ord_123
status: cancelled
"404":
description: Order not found
components:
schemas:
Order:
type: object
required: [id, status]
properties:
id:
type: string
status:
type: string
enum: [pending, paid, shipped]We ran it through @apidevtools/swagger-parser 12.1.0, a Node library for parsing and validating specs. validate() returned the document with no error. That is documented behaviour: the library's options page says validation against the OpenAPI 3 specification rules is not yet supported, so its spec option is ignored for OpenAPI 3 files and only the JSON Schema check runs. The same mistakes in a Swagger 2.0 file do fail; swagger-parser reports Duplicate operation id 'getOrder' and a missing path parameter.
APIForge's design scorer gave the file 100 out of 100, because it grades naming, HTTP usage, versioning and response coverage, not validity. Neither result is wrong, but neither tool checks whether the file is internally consistent. Rule-based linters do. According to their rule documentation, both Redocly CLI and Spectral have a rule for each of the three bugs:
| Bug | Redocly CLI rule | Spectral rule |
|---|---|---|
| Duplicate operationId | operation-operationId-unique | operation-operationId-unique |
| {id} with no path parameter | path-parameters-defined | path-params |
| Example value outside the enum | no-invalid-media-type-examples | oas3-valid-media-example |
Validate from the command line
For the structural layer, a short script around swagger-parser gives you a clear pass or fail and an exit code CI can use. It works for Swagger 2.0, OpenAPI 3.0 and OpenAPI 3.1 files, follows $ref pointers to other files, and fails on any reference that does not resolve.
import SwaggerParser from "@apidevtools/swagger-parser";
const file = process.argv[2] ?? "openapi.yaml";
try {
const api = await SwaggerParser.validate(file);
const version = api.openapi ?? api.swagger;
console.log(`${file} is valid (version ${version}, ${Object.keys(api.paths ?? {}).length} paths)`);
} catch (err) {
console.error(`${file} failed validation:\n${err.message}`);
process.exitCode = 1;
}For the rule layer, pick one linter and commit its config next to the spec, so every developer and the CI job use the same rules. With Redocly CLI, the command is redocly lint followed by the file. It ships rulesets named minimal, recommended, recommended-strict and spec, which you choose with extends in a redocly.yaml file or the --extends flag.
extends:
- recommended
rules:
operation-4xx-response: error
no-unused-components: warnSpectral works the same way with a .spectral.yaml file that extends its built-in OpenAPI ruleset, spectral:oas. Run it with npx @stoplight/spectral-cli lint orders.yaml. Spectral's documentation lists no-$ref-siblings as an error by default, which matters for OpenAPI 3.0 files, where keywords next to a $ref were ignored by most tooling.
extends: ["spectral:oas"]
rules:
operation-success-response: errorYou do not need both linters. They overlap heavily on OpenAPI rules; choose the one whose output format and config style your team prefers, and add rules as you find problems they should have caught.
Run validation in CI
The goal is that a broken spec never merges. Run the checks on pull requests that change the spec file, and make the job required in branch protection. Redocly's github-actions output format turns each problem into an annotation on the changed line, so reviewers see it in the diff.
name: Validate OpenAPI
on:
pull_request:
paths:
- "openapi.yaml"
- "schemas/**"
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- name: Structural validation
run: node scripts/validate-spec.mjs openapi.yaml
- name: Lint rules
run: npx @redocly/cli lint openapi.yaml --format=github-actions- Filter on paths so the job only runs when the spec or its referenced files change. Include every directory that holds split schema files.
- If the spec is generated from code, generate it in CI and validate the output. Validating a committed copy that nobody regenerates checks a stale file.
- Start new rules as warnings on an existing spec, fix the backlog, then promote them to errors. A job that fails on day one with 300 problems gets disabled.
- Pin linter versions in package.json. A new release can add rules or change defaults and fail builds that passed yesterday.
What spec validation does not catch
A file can pass every layer and still mislead the people who use it. Plan for these gaps separately:
- Server behaviour. The spec can say a field is required while the server omits it. Only testing real responses finds that; see how to validate API responses against the OpenAPI schema.
- Weak schemas. type: object with no properties is valid. So is a schema with no required array, which makes every field optional in generated clients. The guide on OpenAPI response design covers what a complete response looks like.
- Breaking changes. Renaming a field produces a valid new file. Catching it means comparing against the previous published version, which is a review step, not a validation step. The API versioning guide covers which changes break clients.
- Wrong descriptions. A validator cannot tell that a description says cents while the server returns dollars.
- Security design. A spec can declare no security at all and still be valid. Review it against the API security best practices.
Swagger 2.0 files
Swagger 2.0 gets slightly better coverage from swagger-parser than OpenAPI 3 does, because the library's rule checks were written for 2.0: duplicate operationIds, missing or extra path parameters, array parameters without items, and duplicate parameters all fail. Linters still support 2.0 with their own rule sets. If you maintain a 2.0 file, the Swagger vs OpenAPI guide covers what changes when you migrate, and the Swagger validator page loads 2.0 files directly.
Check a spec in APIForge
The OpenAPI validator is built for looking at a spec, not for gating a build. Paste a URL to a JSON or YAML file, a Swagger UI page, or upload the file. APIForge parses it, resolves internal references and public HTTP references, and lists every operation with its parameters and schemas. Loading is deliberately lenient: a local reference that does not resolve becomes a placeholder schema labelled Unmodeled schema reference with the pointer, and circular references are ignored instead of failing the load. That lets you explore and run requests against an imperfect spec, and it is the reason to keep a strict validator and a linter in CI.
From the same workspace you can open the 0-100 design score, which flags operations with no 2xx or no error responses, verbs in paths, missing descriptions, missing version prefixes and inconsistent naming. Use it for the fourth layer, alongside the linter config your team agrees on.