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.

Want structured learning?

Take the full Swagger course →