Description
This example shows how to do request validation using FastAPI's Form
.
@router.post("/create", response_class=RedirectResponse)
def create_organization(
name: Annotated[str, Form(
min_length=1,
strip_whitespace=True,
pattern=r"\S+",
description="Organization name cannot be empty or contain only whitespace",
title="Organization name"
)],
Where else in the repository should we be leveraging this pattern to better effect?
Okay, let's analyze the codebase to find other places where leveraging Annotated[..., Form(...)]
for declarative validation would be beneficial.
The pattern is most useful for POST routes that receive form data, particularly simple string inputs where constraints like non-emptiness, length, whitespace stripping, or basic patterns are desired directly in the signature.
Here's a breakdown by router file:
-
routers/account.py
delete_account
:email: EmailStr = Form(...)
:EmailStr
provides good validation already. AddingForm
fordescription
/title
is minor.password: str = Form(...)
: Candidate. Could addmin_length=8
(as a basic check, though complex validation is elsewhere),description
,title
.
password: Annotated[str, Form( min_length=8, # Or match the required minimum description="Account password for verification", title="Password" )],
register
:name: str = Form(...)
: Strong Candidate. Similar to the example, needs non-empty, strip whitespace.
name: Annotated[str, Form( min_length=1, strip_whitespace=True, pattern=r"\S+", description="User's full name", title="Name" )],
email: EmailStr = Form(...)
:EmailStr
is sufficient.password: str = Form(...)
,confirm_password: str = Form(...)
: Candidates. Similar todelete_account
, could addmin_length
,description
,title
. The complex pattern matching is handled by thevalidate_password_strength_and_match
dependency, but basic checks can be added here.
password: Annotated[str, Form( min_length=8, # Match required minimum description="Choose a strong password", title="Password" )], confirm_password: Annotated[str, Form( min_length=8, # Match required minimum description="Confirm your password", title="Confirm Password" )], # Note: The Depends(validate_password_strength_and_match) still handles the comparison and complex pattern.
login
:email: EmailStr = Form(...)
:EmailStr
is sufficient.password: str = Form(...)
: Candidate. Needsmin_length
,description
,title
.
password: Annotated[str, Form( min_length=1, # Basic non-empty check description="Your account password", title="Password" )],
forgot_password
:email: EmailStr = Form(...)
:EmailStr
is sufficient.
reset_password
:email: EmailStr = Form(...)
:EmailStr
is sufficient.token: str = Form(...)
: Candidate. Should not be empty.
token: Annotated[str, Form( min_length=1, strip_whitespace=True, description="Password reset token from email", title="Reset Token" )],
new_password: str = Depends(...)
,confirm_password: str = Form(...)
: Handled by dependency, butconfirm_password
could benefit like inregister
.
confirm_password: Annotated[str, Form( min_length=8, # Match required minimum description="Confirm your new password", title="Confirm New Password" )],
request_email_update
:email: EmailStr = Form(...)
,new_email: EmailStr = Form(...)
: BothEmailStr
are sufficient.
-
routers/organization.py
create_organization
,update_organization
: Already use the pattern forname
.
-
routers/role.py
create_role
:name: str = Form(...)
: Strong Candidate. Needs non-empty, strip whitespace.
name: Annotated[str, Form( min_length=1, strip_whitespace=True, pattern=r"\S+", description="Name for the new role", title="Role Name" )],
organization_id: int = Form(...)
:int
validation is implicit.Form
could addgt=0
,description
,title
.permissions: List[ValidPermissions] = Form(...)
: List validation handled by FastAPI.Form
could adddescription
.
update_role
:id: int = Form(...)
,organization_id: int = Form(...)
,permissions: List[ValidPermissions] = Form(...)
: Similar tocreate_role
.name: str = Form(...)
: Strong Candidate. Same as increate_role
.
name: Annotated[str, Form( min_length=1, strip_whitespace=True, pattern=r"\S+", description="Updated name for the role", title="Role Name" )],
delete_role
:id: int = Form(...)
,organization_id: int = Form(...)
:int
validation is implicit.
-
routers/user.py
update_profile
:name: Optional[str] = Form(None)
: Candidate. Since it's optional, the rules apply if a value is provided.strip_whitespace
is useful. Amin_length
if notNone
could also be added, but is harder to express declaratively.strip_whitespace
is the main benefit here.
name: Annotated[Optional[str], Form( None, # Keep default as None for optionality strip_whitespace=True, # min_length=1, # Can't easily apply conditionally here description="User's updated full name (optional)", title="Name" )],
avatar_file: Optional[UploadFile] = File(None)
: Not applicable forForm
.
Summary of Recommendations:
Apply the Annotated[..., Form(...)]
pattern primarily to:
routers/account.py
:name
inregister
(non-empty, strip whitespace).password
/confirm_password
fields inregister
,reset_password
,login
,delete_account
(at leastmin_length=1
ormin_length=8
,description
,title
).token
inreset_password
(non-empty, strip whitespace).
routers/role.py
:name
increate_role
andupdate_role
(non-empty, strip whitespace).
routers/user.py
:name
inupdate_profile
(strip_whitespace=True
).
This makes the validation constraints clearer directly in the function signature and leverages FastAPI's built-in mechanisms more effectively for basic form field validation, reserving more complex or cross-field validation for dependencies or the route body.