Fuzz the API Against Its OpenAPI Schema
Drive Schemathesis from the OpenAPI document, skip destructive endpoints, and turn every unexpected 500 into a regression test.
Task
Generate malformed and edge-case requests from the API schema to find unhandled 500 responses before clients do.
When to use this
A hand-written suite covers valid requests only. A negative quantity or a Unicode boundary value reaches an unguarded path and returns a 500 with a stack trace.
Prerequisites
- Schemathesis 3.x installed with pipx or available as a Docker image
- An OpenAPI 3 document served at a stable URL
- A disposable staging environment with test data
Steps
1. Install the CLI and confirm the version.
pipx install schemathesis && schemathesis --versionExpected output: A version string such as 3.39.0, confirming the CLI is on PATH.
2. Run every check against staging while excluding endpoints tagged destructive.
schemathesis run https://staging.example.com/openapi.json --checks all --exclude-tag destructive --max-examples 50Expected output: A per-endpoint report; an unexpected 500 is printed with a ready-to-run curl command.
3. Reproduce the failure outside the fuzzer, then encode it as a regression test.
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://staging.example.com/api/orders -H 'Content-Type: application/json' -d '{"quantity":-1}'Expected output: 500 confirmed; after the fix the same request returns 422 and the test asserts that status.
4. Schedule a reproducible nightly run with a fixed seed and more examples.
schemathesis run "$SCHEMA_URL" --checks all --exclude-tag destructive --max-examples 200 --seed 20261021 --workers 4Expected output: The same failures recur on every run, and the reported seed reproduces any new one locally.
5. Fail the pipeline on server errors while allowing expected 4xx responses.
- run: schemathesis run "$SCHEMA_URL" --checks all --exclude-tag destructive
env:
SCHEMA_URL: ${{ secrets.STAGING_SCHEMA_URL }}
Expected output: The job exits non-zero when a request returns 5xx, and passes when invalid input is rejected with 4xx.
Troubleshooting
Problem
The fuzzer writes garbage rows into staging
Solution
Run against an isolated environment, exclude destructive tags and paths, and reset the database after the run.
Problem
Many failures come from endpoints that are not implemented yet
Solution
Exclude those paths or tags explicitly and track them; do not disable checks globally, which hides real 500s.
Problem
The fuzzer reports failures for inputs that violate cross-field rules
Solution
Schemathesis cannot infer rules like startDate < endDate. Treat 4xx as acceptable, and cover cross-field validation with explicit tests.
Limitations
- It only explores what the schema describes; business invariants still need targeted tests.
- Generated data can violate constraints the schema does not express, producing false positives.
- Destructive endpoints must be excluded or run only against an isolated, disposable environment.
Hand-written tests only cover known shapes
Every example test was written by someone who already knew what the endpoint expects. Fuzzing starts from the OpenAPI schema and generates values the author did not think about: negative amounts, extremely long strings, unusual Unicode, missing optional fields. The goal is not to prove the API is correct, but to find inputs that reach an unguarded path and produce a 500. A 500 is almost always a bug: the request was invalid, and the API should have said so with a 4xx.
Start from the contract you already publish
Schemathesis reads the same OpenAPI document your clients use, so the fuzz surface stays aligned with the documented API. It generates valid and invalid requests, checks response codes against the schema, and reports failures with a reproducible curl command. That command is the bridge between fuzzing and a normal regression test: paste it into a test, assert the correct status, and the finding is fixed permanently.
schemathesis run https://staging.example.com/openapi.json \
--checks all --exclude-tag destructive --max-examples 50
Be careful with side effects
Fuzzing POST and DELETE endpoints creates and removes real data. Tag destructive endpoints and exclude them, or run the whole exercise against a disposable environment that is reset afterwards. Authentication is also part of the setup: provide a test token with limited scope, not a production credential, and never point the fuzzer at production.
Make runs reproducible
Random generation is valuable, but debugging needs determinism. Record the seed, the example count, and the schema version in the report. A nightly run with more examples and a fixed seed gives broad coverage while keeping failures repeatable. For pull requests, a smaller example count keeps the job under a few minutes and still catches regressions in changed endpoints.
Act on findings, not on counts
A long list of failures is only useful if it is triaged. Group by root cause: missing validation in one handler, an unhandled null, a database constraint leaking as a 500. Fix the handler and add a regression test per group. Keep the remaining known issues in an explicit exclusion list with an owner, so the nightly job stays green for real regressions and does not become background noise.