Validating Requests Against OpenAPI at the Gateway

An OpenAPI document that only generates documentation is a description of what the API was supposed to be. Loading the same document into the gateway as an enforcement policy makes it a description of what the API is, because a request that does not match it never arrives. This page covers how to get from a specification in a repository to a rule in a proxy, without the two drifting apart and without the first day of enforcement being an outage.

Prerequisite concepts

This assumes the gateway-versus-service split in request validation and schema enforcement, and it uses the contract-publishing mechanics described in consumer-driven contract testing at the gateway.

From document to enforced policy

The specification becomes a build artefact, not a wiki page The OpenAPI document is linted, compiled into per-route schemas, published as a versioned artefact, and loaded by the gateway at deploy. Because the compilation runs in CI, a document that no longer matches the routes fails the build rather than silently diverging from what is enforced. openapi.yaml in the service repo lint + compile one schema per operation versioned artefact immutable, tagged gateway loads it at deploy, not at runtime CI — runs on every change to the document or the routes Fetching the document at runtime from a URL seems simpler and adds a dependency on the request path, a cache with its own staleness question, and a way for a document change to reach production without a deploy. Compile it, version it, ship it — the same discipline as any other artefact the gateway depends on.

What to compile out of the document

Not everything in an OpenAPI document is enforceable, and trying to enforce all of it produces false rejections. Compile the parts that describe the request and are decidable from the message.

# openapi.yaml — the enforceable parts, marked
paths:
  /v2/orders:
    post:
      operationId: createOrder
      parameters:
        - name: x-idempotency-key        # enforceable: required header, pattern
          in: header
          required: true
          schema: { type: string, minLength: 16, maxLength: 64 }
      requestBody:
        required: true
        content:
          application/json:              # enforceable: accepted content types
            schema:
              type: object
              required: [customerId, items]
              additionalProperties: false
              properties:
                customerId: { type: string, format: uuid }
                items:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items: { $ref: '#/components/schemas/LineItem' }
      responses:
        '201': { description: created }  # NOT enforced on the request path
        '409': { description: duplicate }

The response section is documentation for consumers, not a gateway rule. Security schemes are usually better expressed as the gateway’s own auth configuration than derived from the document, because the document says which scheme applies and not how to validate it.

Handling the parts that do not compile cleanly

Three categories, and only one of them is a rule Types, required fields, enumerations, bounds and content types compile directly into enforcement. Discriminated unions, recursive references and format keywords compile with caveats that depend on the validator. Examples, descriptions, links and response definitions carry no enforcement meaning at all. compiles to a rule type, required, enum minimum, maximum, lengths pattern, minItems, maxItems additionalProperties accepted content types compiles with caveats oneOf with a discriminator recursive $ref format: date-time, email, uri nullable and its dialect quirks deeply nested allOf not enforcement at all descriptions and examples response definitions links and callbacks tags and external docs servers The middle column is where validators disagree. Pin the validator version, and test the constructs you rely on against real payloads rather than assuming the dialect matches the one the document was written for.

format deserves specific caution: in JSON Schema it is an annotation rather than an assertion, so whether format: email rejects anything at all depends on the validator and its configuration. If a format matters, express it as a pattern as well.

Keeping the document and the routes in agreement

The document describes operations; the gateway has routes. Nothing forces the two to match, and they diverge the first time somebody adds a route without touching the specification. The fix is a CI check that compares the sets in both directions.

# Fail the build when routes and operations disagree
compiled=$(oas-compile openapi.yaml --list-operations | sort)
routes=$(gw-config list-routes --json | jq -r '.[].name' | sort)

comm -3 <(echo "$compiled") <(echo "$routes") | tee /tmp/drift
[ -s /tmp/drift ] && { echo "route/spec drift detected"; exit 1; }

An operation with no route is dead documentation. A route with no operation is an unenforced endpoint, which is the more dangerous of the two, because it is precisely the endpoint nobody reviewed.

Two sets that must agree, and what each mismatch means Operations in the document but not in the gateway are documentation for endpoints that do not exist. Routes in the gateway but not in the document are endpoints serving traffic with no schema enforcing anything, which is the mismatch worth failing a build over. documented, not routed dead documentation consumers try it and get a 404 both — the healthy case described and enforced the document is the contract routed, not documented unenforced endpoint serving traffic nobody reviewed Fail the build on the right-hand box. The left-hand box is worth a warning: it usually means an endpoint was removed and the specification was not updated, which misleads consumers rather than exposing you. Run the check on every pull request, not nightly — drift is cheapest to fix in the change that caused it.

Decision matrix

Question Answer
Where does the document live with the service, versioned alongside its code
When is it compiled in CI, on every change
How does the gateway get it as a versioned artefact, at deploy
What about responses documented, not enforced on the request path
What about security schemes declared in the document, configured in the gateway
What happens on drift build fails for an unenforced route, warns for a dead operation

Gotchas and failure signals

A document written for humans over-specifies. Descriptions promising “must be a valid ISO country code” mean nothing until they are an enum or a pattern. Enforcement exposes exactly how much of the specification was prose.

Shared component schemas propagate changes further than expected. Tightening one $ref used by nine operations tightens all nine at once, so review by compiled output rather than by document diff.

Validator dialects differ, particularly around nullable, format and how additionalProperties interacts with allOf. Pin the version and test the constructs you actually use.

A document too large to compile quickly slows every deploy. Split by service rather than maintaining one document for the whole platform.

Validation

  • Document lives with the service and is versioned with it
  • Compilation runs in CI and fails on constructs the validator cannot enforce
  • The gateway loads a versioned artefact rather than fetching at runtime
  • Route and operation sets compared in both directions, failing on unenforced routes
  • Validator version pinned, with a test for each construct in use
  • Report-only mode used before enforcement on any existing route

FAQ

Should the gateway fetch the OpenAPI document at runtime?

No. Compile it in CI into a versioned artefact and load that at deploy. Fetching at runtime puts a dependency on the request path, introduces a cache with its own staleness question, and creates a way for a document change to reach production without a deploy or a review. The specification should be an artefact with the same discipline as any other the gateway depends on.

Which parts of an OpenAPI document actually enforce anything?

Types, required fields, enumerations, numeric bounds, string lengths and patterns, array bounds, additionalProperties and the accepted content types. Descriptions, examples, response definitions, links and tags carry no enforcement meaning. In between sit discriminated unions, recursive references and the format keyword, whose behaviour depends on the validator — which is why the validator version should be pinned and the constructs tested.

What happens when routes and documented operations disagree?

Two different problems. An operation with no route is dead documentation that misleads consumers. A route with no operation is an endpoint serving traffic with nothing enforcing its shape, which is the one worth failing a build over — it is precisely the endpoint that nobody reviewed. Compare both sets in CI, on every pull request rather than nightly.

Does format: email actually validate anything?

It depends entirely on the validator and its configuration, because in JSON Schema format is an annotation rather than an assertion. Some validators check it, some ignore it, and some check it differently. If a format matters to your service, express it as a pattern as well so the rule holds regardless of dialect.


Parent: Request Validation & Schema Enforcement