Property-Based Testing of REST APIs from OpenAPI Specs with Schemathesis
You can't write a test for every possible input. A string field accepts billions of values. A numeric parameter has edge cases you haven't considered. An API that looks solid against your 20 hand-written test cases might crash on the 21st input — the one a real user will eventually send.
Property-based testing changes the equation. Instead of specifying inputs, you specify properties that must hold for all inputs. The framework generates the inputs. Schemathesis applies this idea directly to OpenAPI specs, turning your API contract into a test generator.
How Schemathesis Works
Schemathesis reads your OpenAPI spec and automatically generates HTTP requests for every endpoint. For each operation, it:
- Parses the parameter schemas (path params, query params, request body)
- Generates random valid inputs according to those schemas
- Sends hundreds or thousands of requests to your running API
- Checks that responses conform to the spec and that the server doesn't crash
The key insight: your spec already defines what valid inputs look like. Schemathesis uses that definition to explore the input space systematically, including edge cases you wouldn't think to test: empty strings, very long strings, Unicode characters, negative numbers, null values, and schema boundary conditions.
Installation and Basic Usage
pip install schemathesisRun against a live API:
st run http://localhost:8000/openapi.yaml --url http://localhost:8000Or against a hosted spec with a separate target:
st run https://api.example.com/openapi.yaml --url https://staging.api.example.comSchemathesis runs and reports any failures:
FAILURES
POST /api/users
Body: {"email": "", "name": "A" * 10001}
Response: 500 Internal Server Error
Reproduced with:
curl -X POST http://localhost:8000/api/users \
-H "Content-Type: application/json" \
-d '{"email": "", "name": "AAAA..."}'This output tells you exactly what request triggered the bug, with a curl command to reproduce it. You didn't write this test case — Schemathesis found it by exploring the input space.
What Schemathesis Checks Automatically
By default, Schemathesis runs several checks on every response:
not_a_server_error— the server must not return 5xx responses to valid inputsstatus_code_conformance— response status codes must be documented in the speccontent_type_conformance— responseContent-Typemust match the specresponse_schema_conformance— response bodies must validate against their schema
Any violation fails the test. You get a concrete failing example and a full traceback.
Writing Stateful Tests
Stateless property tests hit each endpoint independently. For APIs with state — create a resource, then retrieve it — you need stateful testing. Schemathesis supports OpenAPI links to chain requests:
paths:
/users:
post:
operationId: createUser
responses:
"201":
content:
application/json:
schema:
$ref: "#/components/schemas/User"
links:
GetUserById:
operationId: getUser
parameters:
userId: "$response.body#/id"
/users/{userId}:
get:
operationId: getUser
parameters:
- name: userId
in: path
schema:
type: stringRun with stateful testing enabled:
st run openapi.yaml --url http://localhost:8000 --stateful=linksSchemathesis creates a user, extracts the id from the response, and uses it to fetch that user — testing the complete lifecycle automatically.
Authentication
Most APIs require authentication. Pass headers or tokens:
# Bearer token
st run openapi.yaml --url http://localhost:8000 \
--header <span class="hljs-string">"Authorization: Bearer $TOKEN"
<span class="hljs-comment"># API key
st run openapi.yaml --url http://localhost:8000 \
--header <span class="hljs-string">"X-API-Key: your-key-here"For OAuth or session-based auth, use Schemathesis in Python to hook into the auth flow:
import schemathesis
schema = schemathesis.from_uri("http://localhost:8000/openapi.yaml")
@schema.parametrize()
def test_api(case):
# Inject auth before each request
response = case.call(
headers={"Authorization": f"Bearer {get_token()}"}
)
case.validate_response(response)Running in Python Tests
Schemathesis integrates with pytest:
import schemathesis
schema = schemathesis.from_uri("http://localhost:8000/openapi.yaml")
@schema.parametrize()
def test_api_endpoints(case):
response = case.call_and_validate()Running pytest now generates and executes property-based tests for every endpoint. The @schema.parametrize() decorator creates a test case for each operation, and call_and_validate() sends the request and checks all default properties.
Add custom assertions:
@schema.parametrize()
def test_api_endpoints(case):
response = case.call()
# Standard checks
case.validate_response(response)
# Custom: paginated list responses must include metadata
if response.status_code == 200 and "items" in response.json():
assert "total" in response.json(), "Paginated response missing 'total'"
assert "page" in response.json(), "Paginated response missing 'page'"CI Integration
# .github/workflows/api-property-tests.yml
name: API Property Tests
on: [pull_request]
jobs:
schemathesis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Start API server
run: docker-compose up -d api
- uses: schemathesis/action@v1
with:
schema: "http://localhost:8000/openapi.yaml"
args: "--checks all --hypothesis-max-examples 100"--hypothesis-max-examples 100 controls how many inputs Schemathesis generates per endpoint. Higher values find more bugs but take longer. 100 is a reasonable CI default; run 500+ overnight for deeper coverage.
Shrinking: Minimal Failing Examples
When Schemathesis finds a failure, it "shrinks" the input — systematically reduces it to the smallest input that still triggers the bug. Instead of a 10,000-character string that crashes your server, you get a 1-character string. This makes bugs dramatically easier to debug.
What Schemathesis Catches
Real bugs found by property-based API testing in practice:
- Unhandled null inputs — fields marked optional in the spec cause
NullPointerExceptionwhen actually omitted - Integer overflow — numeric parameters with no
maximumconstraint accept values that overflow database columns - Unicode path parameters — emojis or RTL characters in path params cause routing failures
- Empty string edge cases — required string fields with no
minLengthaccept empty strings that violate business logic - Schema drift — response bodies include undocumented fields, or omit documented required fields
These bugs exist in production APIs that have extensive hand-written test suites. Property testing finds them because it explores the input space systematically rather than testing what developers think to test.
For complete API test coverage — combining property-based testing with scenario-based end-to-end tests — tools like HelpMeTest let you write behavior tests in plain English on top of deployed APIs. Schemathesis and end-to-end tests are complementary: Schemathesis finds spec violations automatically; scenario tests verify business logic intentionally.