cd /news/developer-tools/your-openapi-spec-is-a-contract-nobo… · home topics developer-tools article
[ARTICLE · art-134405] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=↑ positive

Your OpenAPI spec is a contract. Nobody is checking it. I built a CLI that detects spec↔code drift with zero dependencies.

A developer built oas-drift, a zero-dependency Python CLI that detects drift between an OpenAPI spec and a Python codebase by statically analyzing routes with the ast module. The tool reports three drift classes — code-only routes, method mismatches, and spec-only endpoints — and always exits 0, leaving build-failure policy to CI via its JSON output. It was validated against FastAPI's full-stack-fastapi-template, where prefixed router paths produce predictable false-positive pairs.

by read3 min views2 publishedSep 19, 2026

"The generated client keeps calling DELETE /items/{id}."

"The server returns 405 Method Not Allowed."

An OpenAPI spec is not just documentation. SDKs, frontend types, API docs, mock servers — everything is generated from the spec. When the implementation drifts from it, everything generated starts lying quietly.

This is about oas-drift, a CLI I built that detects that drift — with zero dependencies (Python 3.11+ standard library only, no LLM).

https://github.com/sunnydachs/oas-drift

You give it an OpenAPI JSON spec and a Python codebase. It finds three classes of drift:

oas-drift --spec openapi.json

oas-drift --spec openapi.json ./src --json

Here is a real run against a deliberately-drifted demo app:

oas-drift — scanned src
  spec: 5 endpoint(s) | code: 4 route(s)

/health  ➕ CODE ONLY
    route implemented (GET) but not defined in spec — src/app.py:21
/items/{id}  ⚠️ METHOD MISMATCH
    /items/{id}: in spec but not implemented: DELETE; implemented but not in spec: POST — src/app.py:17
/users/{id}  ⚠️ METHOD MISMATCH
    /users/{id}: in spec but not implemented: DELETE — src/app.py:13
/admin/stats  ⬜ SPEC ONLY
    defined in spec (GET) but no matching route in codebase
/items  ⬜ SPEC ONLY
    defined in spec (GET) but no matching route in codebase

summary: {"code_only": 1, "method_mismatch": 2, "spec_only": 2} | ok: 2

"The spec is the contract" only works if someone checks both sides. Code review sees the diff against the last commit — not against a spec written three months ago.

This was the core design decision. Most CI-facing drift tools fail the build when they find anything. oas-drift's exit code is 0 either way. Three reasons:

/health endpoint missing from the spec is usually fine. A METHOD MISMATCH on a payment route is not. Which drift fails the build is policy — the tool shouldn't decide that for you.ast module — never imports, never executes, never writes. Same input → same report, always. If you do want to fail on specific statuses, wire it into CI with --json and jq — the report is machine-readable by design.

Same principles as the sibling tools I shipped this month — doc-drift (README↔code) and plan-drift (tracking plan↔code). Deterministic work deserves deterministic tools.

The detail I obsessed over: path parameters and router prefixes. The rule:

/users/{id} in the spec matches prefix="/items" serving /{id} does not match a spec's /items/{id}. I validated this against a real, widely-used codebase — the backend of FastAPI's official full-stack-fastapi-template (25 files, 14 paths, 23 routes detected). Scanning it with a spec written in prefixed paths produces exactly the false-positive pair this rule predicts:

/items/{id}  ⬜ SPEC ONLY
    defined in spec (DELETE, GET, PUT) but no matching route in codebase
/{id}  ➕ CODE ONLY
    route implemented (DELETE, GET, PUT) but not defined in spec — backend/app/api/routes/items.py:48, ...

Two names for the same route. oas-drift doesn't guess — it reports what literally exists. If your project uses router prefixes, normalize the spec side first (the surest source is the /openapi.json your app actually serves).

And the important detection works: flipping the implemented POST /login/access-token to PUT in a test spec gets reported precisely:

/login/access-token  ⚠️ METHOD MISMATCH
    /login/access-token: in spec but not implemented: PUT; implemented but not in spec: POST — backend/app/api/routes/login.py:23

@app.get(f"/users/{id}") isn't extracted; string literals only. These are documented in the README. The current version prioritizes minimal, honest detection over coverage.

Specs drift the moment the code moves and nobody updates them. A contract is only a contract if something checks it — oas-drift is that checking part, built read-only, fully deterministic, and dependency-free.

This is a personal OSS project with no warranty. If you hit bugs or have suggestions, GitHub issues are the best way to reach me.

── more in #developer-tools 4 stories · sorted by recency
── more on @oas-drift 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/your-openapi-spec-is…] indexed:0 read:3min 2026-09-19 ·