schemathesis-fuzzing

Use whenever an API has an OpenAPI or GraphQL schema and needs tests generated from it: fuzzing every endpoint for 5xx errors and schema violations. Runs Schemathesis from the CLI or pytest, picks the conformance checks and gates CI on failures as the API evolves.

/plugin install qa-api-testing@testland-qa

First time in Claude Code? Add the marketplace with /plugin marketplace add testland/qa

View source (opens in a new tab)

Overview

Schemathesis is a property-based API testing tool that "automatically generates property-based tests from your OpenAPI or GraphQL schema and exercises the edge cases that break your API" (schemathesis-readme (opens in new window)). The schema is the single source of truth - every endpoint, parameter, and response shape becomes a generator that produces hundreds of targeted variations per run.

This is complementary to example-based API testing (postman-collections, tavern-testing, restassured-testing, karate-testing) - example-based tests verify happy paths; Schemathesis attacks the boundaries the team forgot.

When to use

  • The repo has an OpenAPI 2.0 / 3.0 / 3.1 / 3.2 spec or a GraphQL schema (June 2018+).
  • The team wants automatic test coverage of new endpoints without authoring per-endpoint tests.
  • Negative-scenario coverage ("what happens with a malformed request?") is missing or thin.
  • A CI gate on 5xx responses (any unexpected server error) is desired.

If the API has no schema, Schemathesis cannot help - generate a schema first (FastAPI, Flask-Smorest, NestJS Swagger, dropwizard, or hand-author a spec), then return.

How to use

  1. Confirm the API ships an OpenAPI 2/3.x or GraphQL schema (see When to use).
  2. Install the CLI (pip install schemathesis, or uvx for a one-off run).
  3. Run schemathesis run <schema> against staging with the default checks; reproduce any failure from the printed curl command + Hypothesis seed.
  4. Promote it to a first-class pytest test (@schema.parametrize()) and gate CI on schema-conformance + 5xx detection - deep CI wiring and advanced auth hooks live in references/ci-and-pytest-integration.md.

Install

pip install schemathesis

(Per schemathesis-readme (opens in new window).)

For the latest CLI without modifying the project's Python env:

uvx schemathesis run <schema-url>

(Adapted from schemathesis-docs (opens in new window) - uvx is the uv runner for one-off tool execution.)

Running via CLI

Basic invocation per schemathesis-readme (opens in new window):

schemathesis run <schema-url>

<schema-url> can be:

  • Remote URL: https://api.example.com/openapi.json.
  • Local file: ./openapi.yaml.

Key flags

Per schemathesis-readme (opens in new window):

FlagPurpose
--base-url <url>Override the API base URL (test against staging vs prod).
--checks <name> (repeatable)Restrict to specific validations.
--hypothesis-max-examples <N>Number of generated cases per endpoint.
--workers <N>Parallel workers; speeds up large schemas.
--header 'X-API-KEY: ...'Inject auth header on every generated request.
--auth user:passHTTP Basic Auth.
--cassette-har <path>Record / replay using HAR files (debug aid).

Worked example

schemathesis run https://api.example.com/openapi.json \
  --base-url https://staging.example.com \
  --checks status_code_conformance \
  --checks response_schema_conformance \
  --checks not_a_server_error \
  --hypothesis-max-examples 200 \
  --workers 4 \
  --header "Authorization: Bearer $API_TOKEN"

Built-in checks

Per schemathesis-readme (opens in new window), the canonical checks:

CheckWhat it verifies
status_code_conformanceResponse status code is one of the codes documented in the schema for that endpoint.
response_schema_conformanceResponse body matches the documented schema (types, required fields, enum values).
content_type_conformanceContent-Type header is one of the schema's documented media types.
response_headers_conformanceResponse headers conform to the schema's headers declaration.
not_a_server_errorThe response is not in the 5xx range; any 5xx is a hard fail.

Run all checks (default), or restrict via repeated --checks:

# Strict: every check active
schemathesis run <schema>

# Only flag 5xx errors (cheap smoke test)
schemathesis run <schema> --checks not_a_server_error

A failing check produces a deterministic reproduction - Schemathesis prints the exact curl command and Hypothesis seed to reproduce the generated request.

Pytest integration

For projects that want Schemathesis cases as first-class pytest tests (schemathesis-readme (opens in new window)):

# tests/api/test_schemathesis.py
import schemathesis

schema = schemathesis.openapi.from_url("https://your-api.com/openapi.json")

@schema.parametrize()
def test_api(case):
    case.call_and_validate()

@schema.parametrize() generates one pytest test per endpoint × method combination. case.call_and_validate() issues the generated request and runs every default check.

For project-specific auth injection, header signatures, or response post-processing, use before_call hooks - see references/ci-and-pytest-integration.md.

Operating in CI

Gate PRs on Schemathesis with a shallow per-PR run (--hypothesis-max-examples 50) and a deeper nightly cron (200+), always against staging via --base-url. Hypothesis shrinks any failure to a minimal reproducer, so a 50-example PR run catches most regressions while the nightly depth surfaces the rare ones. The full GitHub Actions workflow (JUnit reporting, artifact upload) and the per-PR / nightly / weekly cadence table live in references/ci-and-pytest-integration.md.

Anti-patterns

Anti-patternWhy it failsFix
Running with --checks not_a_server_error only and calling it done5xx detection misses 4xx-with-wrong-content cases.Run all default checks; 5xx-only is a smoke check, not coverage.
--hypothesis-max-examples 5 to keep CI fastCoverage too thin; flaky-looking results.50+ on PR; if too slow, parallelize via --workers.
Targeting production URLGenerated requests can mutate prod data; 5xx alerts trigger oncall.Always --base-url <staging>; production should never see fuzz traffic.
Stale schema URLSchemathesis fuzzes the schema's authoritative version, not what the deploy actually serves; false negatives mask bugs.CI fetches the schema from the PR's deployed staging artifact, not from a checked-in copy.
Ignoring shrunk failuresA not_a_server_error failure with a 5-byte input is a real bug, not noise.Triage every failure; close as "won't fix" only with a documented schema-amendment plan.

Limitations

  • Authentication state. Schemathesis can attach a token but doesn't model multi-step auth flows (login → token → use). Combine with tavern-testing or restassured-testing for the auth chain.
  • Stateful sequences. Out of the box, every Schemathesis case is independent. For stateful API fuzzing (POST → GET created resource → DELETE), use restler-fuzzing, which is built for stateful sequences.
  • Schema drift. If the schema doesn't match the implementation, every case fails for the wrong reason. Run a Schemathesis baseline pre-PR to catch schema drift early.
  • OpenAPI 2.0 limitations. Some OpenAPI 2.0 features (e.g. formData) generate weaker variations than 3.x; consider migrating the schema to 3.x.

References

  • schemathesis-readme (opens in new window) - main repo: install, CLI flags, built-in checks, pytest integration with @schema.parametrize().
  • schemathesis-docs (opens in new window) - full docs (CLI reference, hooks, authentication strategies).
  • restler-fuzzing - stateful fuzzing complement.
  • postman-collections, tavern-testing, restassured-testing, karate-testing - example-based authoring; use alongside Schemathesis for happy-path coverage.
Reference

Schemathesis CI wiring and advanced pytest hooks

View source (opens in a new tab) (opens in new window)

Deep reference for schemathesis-fuzzing SKILL.md. Consult when wiring Schemathesis into CI, or when the pytest integration needs project-specific auth / header injection.

Advanced pytest integration - hooks

For finer-grained integration than the basic @schema.parametrize() shown in SKILL.md (schemathesis-readme (opens in new window)):

@schema.parametrize()
@schemathesis.hook("before_call")
def add_auth(context, case):
    case.headers["Authorization"] = f"Bearer {os.environ['API_TOKEN']}"

def test_api_with_auth(case):
    case.call_and_validate()

Hooks let the team inject project-specific auth, header signatures, or response post-processing without forking Schemathesis.

CI integration

# .github/workflows/api-fuzz.yml
name: api-fuzz

on:
  pull_request:
  push:
    branches: [main]
  schedule:
    - cron: '0 6 * * *'   # nightly broader run

jobs:
  schemathesis:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - run: pip install schemathesis

      - name: Schemathesis run
        env:
          API_TOKEN: ${{ secrets.STAGING_API_TOKEN }}
        run: |
          schemathesis run https://staging.example.com/openapi.json \
            --base-url https://staging.example.com \
            --hypothesis-max-examples 50 \
            --workers 4 \
            --header "Authorization: Bearer $API_TOKEN" \
            --junit-xml=results.xml

      - name: Upload report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: schemathesis-results
          path: results.xml
          retention-days: 14

      - name: Surface JUnit
        if: always()
        uses: dorny/test-reporter@v1
        with:
          name: API fuzz
          path: results.xml
          reporter: java-junit

The PR-trigger run uses lower --hypothesis-max-examples (e.g. 50) for fast feedback; the nightly cron run uses higher values (200+) for deeper coverage. This separation keeps PR CI under 10 minutes without sacrificing nightly depth.

Two complementary CI cadences

CadenceExamples per endpointPurpose
Per-PR50Fast smoke; catch obvious schema-drift breaks.
Nightly200-500Deep coverage; surface rarely-triggered edge cases.
Weekly1000+Pre-release deep validation.

Hypothesis (the underlying property-based engine) shrinks failures to minimal reproducers automatically - a 200-example PR run is plenty to surface most regressions; nightly depth catches the rare ones.

  • 41 of 44 operations red, and a branch that turns every one of them green
  • Every cart case comes back 401 or 404, and there are two proposals for making that stop
  • A deadline PR points the generated-case job at the live API and calls it read-only
  • Ninety green nights, and a customer questionnaire that Marcus has already filled in
  • 214 green nights, four open support clusters, and an instruction to cut the job down to an alarm