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-flowCLI). 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.
writebackdelivers the parsed payload — coercions applied, defaults filled in. Path parameters are replaced in the view's arguments directly; body and query land ong.doctreen_validated(Flask'srequest.argsis immutable and its JSON is cached).responseis 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 thanstr | Noneon 3.9 — Pydantic evaluates annotations at runtime and|needs 3.10. - A runnable example lives at
examples/flask_app.py.