Gate a FastAPI endpoint with a contract¶
Reject malformed bodies before they reach your handler. The packaged dependency resolves the contract once at startup and reuses the compiled validator for every request:
from fastapi import Depends, FastAPI
from pycharter.contrib.fastapi import contract_dependency
app = FastAPI()
# Built once — the Validator caches the compiled Pydantic model and is
# safe to share across FastAPI's worker threads.
validate_order = contract_dependency("contracts/orders.yaml")
@app.post("/orders")
async def create_order(order: dict = Depends(validate_order)) -> dict:
# `order` here is the coerced + validated payload — same shape as
# the contract's json_schema.
return {"status": "accepted", "order_id": order["order_id"]}
Contract violations return a 422 whose body carries the violations so the caller can debug:
contract_dependency also accepts an existing
Validator instance when you want to share one
across routes or configure it with the Validator constructor's
keyword options (strict, include_quality, state_rules).
What it does under the hood¶
The dependency is ten lines — write your own variant when you need different semantics (custom status codes, quality tracking, headers):
from fastapi import Depends, FastAPI, HTTPException
from pycharter import Validator
app = FastAPI()
order_validator = Validator.from_file("contracts/orders.yaml")
def validate_order(payload: dict) -> dict:
result = order_validator.validate(payload)
if not result.is_valid:
# Return the contract violations as the 422 body so the caller
# can debug. Result.errors is JSON-serialisable.
raise HTTPException(status_code=422, detail={"errors": result.errors})
return result.data.model_dump()
@app.post("/orders")
async def create_order(order: dict = Depends(validate_order)) -> dict:
return {"status": "accepted", "order_id": order["order_id"]}
Why this beats class Order(BaseModel) declared inline:
- The same contract YAML is consumed by your ETL pipeline, your background worker, and the JS client SDK. Change the contract once; every surface picks it up.
- You can add coercion (
"100" → 100) and custom business rules (amount >= 0.01) via the contract instead of writing field validators in code. pycharter contract diff old.yaml new.yaml --fail-on-breakingruns in CI so a contract change can't ship without a review.
For request-by-request quality metrics, hand each
Validator.validate(...) result to a
QualityPrometheusObserver at the end
of the handler.
See also: Validator class reference.