Forms¶
Not every hunt starts with JSON. Browsers, legacy clients, and webhook payloads
often arrive as application/x-www-form-urlencoded or multipart/form-data.
VelociPy treats those fields as typed parameters, no different from query strings and headers:
with typed, declarative parameters.
The Form(...) marker tells the framework to extract a value from the request
body, coerce it to the declared Python type, and pass it to your handler. If a
required field is missing or the type doesn't fit, you get a clean 422 response
before your code runs.
No extra dependency for simple forms
URL-encoded form fields work out of the box. Multipart uploads with
UploadFile require the multipart extra; see Uploads
for that.
Basic form fields¶
Declare each field with Annotated[..., Form(...)]:
from typing import Annotated, Any
from velocipy import Form, VelociPy
app = VelociPy()
@app.post("/login")
async def login(
username: Annotated[str, Form(...)],
password: Annotated[str, Form(...)],
) -> dict[str, Any]:
return {"username": username}
A request with body username=alice&password=secret yields:
Missing username or password returns 422 Unprocessable Entity.
Defaults and optional fields¶
Use Form(default) to make a field optional or give it a default value:
@app.post("/search")
async def search(
q: Annotated[str, Form("")],
limit: Annotated[int, Form(10)],
) -> dict[str, Any]:
return {"q": q, "limit": limit}
| Annotation | Effect |
|---|---|
Form(...) |
Required field; 422 if missing. |
Form("") |
Optional string, defaults to empty. |
Form(10) |
Optional integer, defaults to 10. |
Form(False) |
Optional boolean, coerced from "true" / "false". |
VelociPy coerces form values using the same rules as query parameters: integers, floats, booleans, and strings are all supported.
Use typed defaults
Declaring limit: Annotated[int, Form(10)] gives you an int in the
handler. The framework converts the raw string "5" to 5 automatically.
Boolean and numeric coercion¶
Boolean fields accept common string representations:
@app.post("/preferences")
async def preferences(
remember_me: Annotated[bool, Form(False)],
dark_mode: Annotated[bool, Form(False)],
) -> dict[str, bool]:
return {"remember_me": remember_me, "dark_mode": dark_mode}
These are all interpreted as True:
remember_me=trueremember_me=1remember_me=yes
And these as False:
remember_me=falseremember_me=0remember_me=- missing field
Form models¶
If you prefer to group fields into a model, you can define a msgspec.Struct
or pydantic.BaseModel and declare it as a form parameter. VelociPy's
serialization adapters validate the body against the model:
from pydantic import BaseModel
from typing import Annotated
from velocipy import Form, VelociPy
class LoginForm(BaseModel):
username: str
password: str
remember_me: bool = False
app = VelociPy()
@app.post("/login-model")
async def login_model(form: Annotated[LoginForm, Form(...)]):
return {"username": form.username, "remember_me": form.remember_me}
Model-backed forms may need the multipart extra
Model validation of form bodies works through the configured serialization
adapter. Multipart file uploads still require pip install -e ".[multipart]".
Aliases and descriptions¶
Like Query, Header, and Cookie, Form supports alias and description:
@app.post("/subscribe")
async def subscribe(
email: Annotated[str, Form(..., description="Subscriber email")],
list_id: Annotated[str, Form(..., alias="list-id")],
):
return {"email": email, "list_id": list_id}
The alias is used when reading the form field, while the parameter name is what your handler works with.
Mixing form and other parameters¶
Form fields can coexist with query parameters, headers, and path parameters in the same handler:
from typing import Annotated
from velocipy import Form, Header, Query, VelociPy
@app.post("/items/{item_id}")
async def update_item(
item_id: int,
name: Annotated[str, Form(...)],
category: Annotated[str, Query("")],
x_request_id: Annotated[str | None, Header()] = None,
):
return {
"item_id": item_id,
"name": name,
"category": category,
"request_id": x_request_id,
}
The framework sorts every parameter by its marker: path values from the route, query from the URL, headers from the request, and form values from the body.
At a glance¶
| Feature | Supported? | How |
|---|---|---|
| Required fields | Yes | Form(...) |
| Typed defaults | Yes | Form(default) |
| Booleans / ints / floats | Yes | Automatic coercion |
| Field aliases | Yes | Form(..., alias="...") |
| Model validation | Yes | Annotated[Model, Form(...)] |
| Multipart uploads | Yes | UploadFile with multipart extra |
| OpenAPI docs | Yes | Generated automatically |
See also¶
examples/form_parameters.pyfor a runnable form-fields demo.- Requests & responses for query, header, cookie,
body parameter patterns, and multipart file uploads with
UploadFile.