DocTreen
Adapters

Flask

Python adapter — docs UI, OpenAPI 3.1, validation, and drift on the Flask app you already have.

Package status: alpha. Flask works end to end — docs UI, OpenAPI 3.1 export, runtime validation, schema drift detection, and flows (pytest plugin + doctreen-flow CLI). The mock server and codegen are still planned on the Python side.

pip install "doctreen[flask,pydantic]"

Mount

flask_adapter returns a Blueprint. Register it anywhere — route introspection happens on the first docs request, so routes registered after the blueprint are still picked up:

from flask import Flask
from doctreen.adapters.flask import flask_adapter

app = Flask(__name__)

app.register_blueprint(flask_adapter(app, {
    "meta": {"title": "My API", "version": "1.0.0"},
}))

That serves GET /docs (the interactive UI) and GET /docs/openapi.json. The config dict takes the same camelCase keys as every other adapter.

Declare routes with @define_route

@define_route attaches metadata and returns your function unchanged — it is not a wrapper and cannot change how the route behaves. Schemas may be Pydantic models or the s.* builder:

from pydantic import BaseModel
from doctreen.adapters.flask import define_route


class CreateUser(BaseModel):
    name: str
    email: str


class User(CreateUser):
    id: int


@app.post("/users")
@define_route(
    description="Create a user",
    request={"body": CreateUser},
    response=User,
    errors={409: "Email already in use"},
)
def create_user():
    ...

Accepted keyword arguments mirror defineRoute: description, headers, request (body / query / params), response (single schema or a status-keyed map like {201: Created, 200: Existing}), errors, validate, hidden, security, tags, examples, callbacks.

Free path-parameter types

Flask's URL converters carry typing information the Node adapters never had — /users/<int:user_id> documents user_id as a number with nothing to declare. int and float map to number; uuid, string, path, and any map to string.

Runtime validation

app.register_blueprint(flask_adapter(app, {
    "validate": {"writeback": True, "response": "warn"},
}))

Requests that do not match a route's declared Pydantic model are rejected with the structured 422 envelope before the handler runs. Every part is checked in one pass, so a bad body and a bad query report both at once.

  • writeback delivers the parsed payload — coercions applied, defaults filled in. Path parameters are replaced in the view's arguments directly; body and query land on g.doctreen_validated (Flask's request.args is immutable and its JSON is cached).
  • response is status-aware and dev-only: "warn" logs a mismatch, "throw" surfaces it, "off" (default) does nothing.

Validation needs a real parser, so it applies to routes declared with Pydantic models. A route declared with s.* builders is documented but not validated.

Schema drift detection

app.register_blueprint(flask_adapter(app, {
    "drift": {"enabled": True, "sampleRate": 0.01},
}))

Mismatches against real traffic are aggregated at GET /docs/drift.json — same report format as every other adapter.

On multi-process deployments: under gunicorn or uWSGI each worker holds its own in-memory store, and /docs/drift.json reports whichever worker answered. Anything beyond development wants a shared store — implement record / report / reset (plus optional announce_routes) and pass it as {"drift": {"store": RedisDriftStore()}}.

Notes

  • Python 3.9+: write Pydantic fields as Optional[str] rather than str | None on 3.9 — Pydantic evaluates annotations at runtime and | needs 3.10.
  • A runnable example lives at examples/flask_app.py.

On this page