# Request Data and Dependencies

Bind and validate typed request data, dataclass models, forms, and reusable dependencies.

Source: https://flasgo.dev/guides/request-data-and-dependencies/

Flasgo uses `typing.Annotated` markers to bind request data without requiring a separate validation package. Path parameters still come from the route, and unmarked non-path parameters remain query parameters for compatibility.

## Typed request data

```python
from dataclasses import dataclass
from typing import Annotated

from flasgo import Body, Cookie, Depends, Flasgo, Header, Query


@dataclass
class CreateWidget:
    name: str
    quantity: int


def page_size(
    limit: Annotated[int, Query(alias="page-size")] = 20,
) -> int:
    return limit


app = Flasgo()


@app.post("/widgets")
def create_widget(
    payload: Annotated[CreateWidget, Body()],
    limit: Annotated[int, Depends(page_size)],
    client_version: Annotated[int, Header()] = 1,
    session_id: Annotated[
        str,
        Cookie(alias="session-id"),
    ] = "anonymous",
) -> dict[str, object]:
    return {
        "name": payload.name,
        "quantity": payload.quantity,
        "limit": limit,
        "client_version": client_version,
        "session_id": session_id,
    }
```

The markers select the wire source:

| Marker | Source and behavior |
| --- | --- |
| `Body()` | One JSON body, usually a standard-library dataclass model |
| `Query()` | A query-string value; supports an explicit alias |
| `Header()` | A request header; underscores map to hyphens unless an alias is supplied. The reserved `Accept`, `Authorization`, `Content-Type`, and `Cookie` names raise `ValueError` at registration |
| `Cookie()` | A scalar cookie value; uses the exact parameter name unless aliased |
| `Form()` | One URL-encoded or multipart dataclass model |
| `Depends()` | A value returned or yielded by a provider; see lifetimes below |

Use markers inside `Annotated`; using a marker as the parameter default is rejected so static type checkers continue to see the real default value.

Put the marker on the outer annotation and the optional type inside it: write `Annotated[str | None, Header()] = None`, not `Annotated[str, Header()] | None`. In 0.10.0, a marker nested inside another type is rejected at route registration; earlier releases ignored it and read the value from the query string.

## Models and validation

Dataclass fields may use scalar types, nested dataclasses, collections, tuples, unions, literals, and enums. Query, header, cookie, form, and untyped path text is converted to the declared type, so `item_id: int` on `/items/<item_id>` accepts `/items/5`. Numeric text must use plain ASCII decimal syntax such as `12`, `-3`, or `1.5e3`; `1_000`, surrounding spaces, and non-ASCII digits are rejected. Unknown model fields, invalid media types, ambiguous duplicate scalar headers or cookies, and invalid values are rejected. JSON parsing also rejects excessive nesting, oversized integers, and non-finite constants such as `NaN` and `Infinity` with `400 Bad Request`.

JSON values are not converted across types. `Literal`, `Enum`, and `UUID` fields require the exact JSON type, so `true` does not match `Literal[1]` and a UUID must be a JSON string. An integer too large for a `float` field, a `set[Model]` whose items are unhashable, and a dataclass `__post_init__` that raises `TypeError` or `ValueError` all return `422` rather than `500`.

Validation failures return `422 Unprocessable Content` with bounded field locations, codes, and messages. Collections, mappings, and models report every invalid item up to `MAX_VALIDATION_ISSUES`, and an invalid value for `Model | None` reports the model's own field errors. Rejected values and internal type names are not reflected in the default response. The same endpoint plan drives both runtime binding and OpenAPI generation, keeping aliases, required fields, reusable schemas, and validation responses aligned.

Only one `Body()` or `Form()` input may appear across an endpoint and its dependency graph. This keeps request-body consumption deterministic.

## Validated forms and uploads

`Form()` accepts a dataclass and supports both URL-encoded and multipart requests:

```python
from dataclasses import dataclass
from typing import Annotated

from flasgo import Form, UploadedFile


@dataclass
class ProfileForm:
    display_name: str
    interests: list[str]
    avatar: UploadedFile | None = None


@app.post("/profiles")
async def create_profile(
    profile: Annotated[ProfileForm, Form()],
):
    return {
        "display_name": profile.display_name,
        "interests": profile.interests,
        "avatar": (
            profile.avatar.filename if profile.avatar else None
        ),
    }
```

`FormValidationError` retains the parsed `FormData` so an explicit error handler can redisplay safe form fields. Its `errors` property maps each form field name to its messages, grouping errors for individual list items under the list field's name, and `issues` holds the raw `ValidationIssue` tuple. Since 0.11.0, a form model whose `__post_init__` raises `ValueError` or `TypeError` also produces a `422` `FormValidationError` that reaches form-error handlers with the parsed form, matching `Body()`.

Uploaded-file fields may be a single `UploadedFile`, an optional file, or a `list`, `set`, or `tuple` of files. A fixed-length tuple such as `tuple[UploadedFile, UploadedFile]` requires exactly that many files, and every member type must accept an `UploadedFile`. Upload filenames have path components and control characters removed, followed by repeated trimming of surrounding whitespace and dots; a name that sanitizes to nothing becomes `upload`. Treat the result as an untrusted display name, generate your own storage name, and do not redisplay uploaded file contents.

URL-encoded forms decode percent-escapes with the declared `charset` (UTF-8 by default); invalid bytes return `400 Bad Request` instead of being replaced.

Query strings, URL-encoded forms, and multipart text fields are capped by `MAX_FORM_FIELDS` (`1000` by default). Multipart requests are also capped by `MAX_MULTIPART_PARTS` (`1000` by default) before full parsing. Requests over either limit receive `413 Content Too Large`.

## Dependency injection

Providers may be synchronous or asynchronous, may depend on other providers, and are cached once per request by default:

```python
from typing import Annotated

from flasgo import Depends, Header


async def tenant_id(
    tenant: Annotated[str, Header(alias="x-tenant-id")],
) -> str:
    return tenant


@app.get("/tenant")
async def tenant_summary(
    tenant: Annotated[str, Depends(tenant_id)],
):
    return {"tenant": tenant}
```

A provider that returns an instance of the annotated dataclass, including `User`, is injected as-is rather than rebuilt from a mapping, so managed resources on it are not copied. Use `Depends(provider, use_cache=False)` only when repeated execution is intentional. Dependency cycles and conflicting duplicate wire-parameter schemas are rejected. Declare providers at module scope when using postponed annotations so Python can resolve their names reliably. When an annotation cannot be resolved at runtime, such as a name imported only under `TYPE_CHECKING`, unmarked handler parameters and return types fall back to forward references instead of failing registration, and a dataclass model keeps validating its other fields.

## Dependency lifetimes and overrides

The `database_session` example below expects an application-owned database pool on `app.state.database`, whose `session()` method is an async context manager. Flasgo does not supply a database driver or ORM.

```python
from typing import Annotated, Protocol, runtime_checkable
from flasgo import Depends, Flasgo

app = Flasgo()


@runtime_checkable
class Database(Protocol):
    async def list_public_users(self) -> list[dict[str, object]]: ...


async def database_session():
    async with app.state.database.session() as database:
        yield database


@app.get("/users")
async def users(database: Annotated[Database, Depends(database_session)]):
    return await database.list_public_users()
```

Providers may return an ordinary value, return an awaitable, or yield exactly one value from a sync/async generator.
Use `try/finally` or a context manager inside the provider for cleanup. Application exceptions reach the provider at
its `yield`; providers must re-raise exceptions rather than suppressing them. Resources close in reverse acquisition
order, including when binding, validation, or the handler fails.

- `Depends(provider, scope="request")`, the default, retains the resource until response sending finishes or fails.
  It supports a resource used by a streaming producer. Cleanup runs before background tasks.
- `Depends(provider, scope="function")` closes the resource before response headers are sent. Use this scope for
  transaction boundaries whose commit failure must prevent a successful response.
- A request-scoped provider cannot depend on a function-scoped provider. Cache identity includes the declared scope;
  `use_cache=False` acquires a separate value/resource for each binding.
- A request-scoped cleanup failure after response delivery is logged; the framework cannot retract an already sent
  response. Critical transactions must finish before sending. Background tasks must acquire their own resources.

Ordinary synchronous providers retain their existing execution behavior. Keep their work short and nonblocking; use
async database/network APIs for I/O. Cleanup and cancellation require cooperative application code.

Routes and blueprints accept `dependencies=[Depends(...)]` for providers whose result is not injected into a handler
parameter, such as an audit or prerequisite check. Their request parameters participate in binding and OpenAPI.
Inherited authorization and rate checks execute before these dependencies.

See the [dependency override test](/guides/testing/#dependency-overrides) for a complete replacement-provider example.

Overrides are context-local, nest safely, and restore automatically. They do not mutate global provider registrations
or the public OpenAPI contract. Concurrent async tests can use different overrides on the same application. Replacement
providers undergo the same binding, lifetime, and cycle checks. Declare providers at module scope when using postponed
string annotations; Python 3.14's native deferred annotations also work.

## Validation budgets

Every request shares bounded validation budgets:

- `MAX_VALIDATION_DEPTH` limits recursive model and collection depth.
- `MAX_VALIDATION_WORK` limits total validation effort, including union branches.
- `MAX_VALIDATION_ISSUES` limits returned errors.

When a budget is reached, Flasgo stops further work and returns one bounded `validation_limit` or `too_many_errors` issue. Keep these limits enabled and add tests for deeply nested, oversized, and invalid payloads.

Dataclass handler return values are converted recursively to JSON. Underscore-prefixed fields are omitted at every dataclass level so private implementation fields are not serialized accidentally; ordinary mapping keys remain application data and are not filtered. JSON responses reject non-finite floats instead of emitting non-standard JSON.

Return annotations describe OpenAPI. To enforce an output schema and filter fields, opt into [Response Contracts](/guides/response-contracts/) with `response_model=`.

Related docs:

- [HTTP Applications](/guides/http-apps/)
- [API Documentation](/reference/api-documentation/)
- [Testing](/guides/testing/)

Sources:

- [README.md](https://github.com/L1ghtn1ng/flasgo/blob/main/README.md)
- [flasgo/params.py](https://github.com/L1ghtn1ng/flasgo/blob/main/flasgo/params.py)
- [flasgo/validation.py](https://github.com/L1ghtn1ng/flasgo/blob/main/flasgo/validation.py)
