Skip to content

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:

pip install pycharter[server]
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:

{"detail": {"errors": ["amount: Field required"]}}

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-breaking runs 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.