OpenAPI specifications are more than just documentation; they’re executable contracts.
Let’s see this in action. Imagine a simple OpenAPI spec for a basic user service:
openapi: 3.0.0
info:
title: User Service API
version: 1.0.0
paths:
/users:
get:
summary: Get a list of users
responses:
'200':
description: A list of users
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: integer
name:
type: string
post:
summary: Create a new user
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
required:
- name
responses:
'201':
description: User created successfully
content:
application/json:
schema:
type: object
properties:
id:
type: integer
name:
type: string
/users/{userId}:
get:
summary: Get a specific user
parameters:
- name: userId
in: path
required: true
schema:
type: integer
responses:
'200':
description: User found
content:
application/json:
schema:
type: object
properties:
id:
type: integer
name:
type: string
'404':
description: User not found
Now, what problem does validating this solve? It prevents inconsistencies between your API’s actual implementation and its documented contract. Without validation, you might have a client expecting a GET /users endpoint to return a 200 with a JSON array of user objects, only to find out the server implementation changed and now returns a 500 with a plain text error message. CI validation catches this before it hits production.
Internally, these validation tools parse your OpenAPI document and apply a set of rules. These rules check for syntactic correctness, adherence to OpenAPI specifications, and often, semantic consistency. For example, they’ll ensure that all required fields are actually defined, that data types match between request and response schemas, and that parameters are correctly specified.
The exact levers you control are primarily within the configuration of the validation tools you choose. For instance, if you’re using a linter like Spectral, you define rulesets. A typical ruleset might look like this:
extends: spectral:oas
rules:
operation-operationId-unique: true
operation-summary:
description: All operations must have a summary.
severity: error
given: $.paths[*].operations[*].summary
then:
function: truthy
info-contact:
description: API must have contact information.
severity: warn
given: $.info.contact
then:
function: truthy
path-declarations-must-exist:
description: Path parameters must be declared in the path.
severity: error
given: $.paths[*].parameters[*][?(@.in=="path")]
then:
function: pathParamDefined
Here, operation-operationId-unique: true enforces that every operation ID is unique across the API. operation-summary ensures every operation has a summary field, which is crucial for documentation generation. info-contact is a warning to ensure contact details are present, promoting discoverability. path-declarations-must-exist is a more complex rule that checks if parameters declared in the parameters section for a path actually appear in the path string itself (e.g., /users/{userId}).
When you integrate this into your CI pipeline (e.g., GitHub Actions, GitLab CI), you’ll typically have a step that runs a command like npx @openapitools/openapi-generator-cli lint --input spec.yaml or spectral lint spec.yaml --ruleset rules.yaml. This command will analyze your spec.yaml file against the defined rules. If any rule is violated, the CI job fails, preventing a broken API contract from being deployed.
You can also extend validation to test the actual running API against the spec. Tools like Dredd can act as a middleware, intercepting requests and responses between your client and server, and validating them against the OpenAPI document. This is a powerful way to ensure your implementation truly conforms.
The most surprising thing about OpenAPI validation is how often it reveals subtle, yet critical, discrepancies that would otherwise go unnoticed until runtime, often impacting end-users. It’s not just about catching typos; it’s about enforcing a precise, machine-readable agreement that governs API behavior. For instance, a common oversight is defining a response schema that includes an optional field, but the actual implementation always returns that field. A strict validator will flag this as a deviation, forcing you to either update the spec or the implementation to align perfectly with the contract.
Beyond linting, you can use tools to generate code from your OpenAPI spec. This generated code can then be used to build clients or server stubs. Running your actual API implementation against these generated clients in a CI test suite provides another layer of validation, ensuring runtime compatibility.
The next step after ensuring your OpenAPI spec is lint-free and validated against its implementation is to consider how you manage breaking changes and versioning within your API.