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

> Source: <https://dev.to/sunnydachs/your-openapi-spec-is-a-contract-nobody-is-checking-it-i-built-a-cli-that-detects-spec-code-drift-23ah>
> Published: 2026-09-19 07:13:34+00:00

"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](https://github.com/sunnydachs/oas-drift)

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

```
# scan the current directory against a spec (read-only)
oas-drift --spec openapi.json

# scan a specific root, machine-readable output
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.*
