Skip to content

Make better use of FastAPI's Annotated[..., Form(...)] pattern for request validation #110

Open
@chriscarrollsmith

Description

@chriscarrollsmith

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:

  1. routers/account.py

    • delete_account:
      • email: EmailStr = Form(...): EmailStr provides good validation already. Adding Form for description/title is minor.
      • password: str = Form(...): Candidate. Could add min_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 to delete_account, could add min_length, description, title. The complex pattern matching is handled by the validate_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. Needs min_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, but confirm_password could benefit like in register.
      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(...): Both EmailStr are sufficient.
  2. routers/organization.py

    • create_organization, update_organization: Already use the pattern for name.
  3. 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 add gt=0, description, title.
      • permissions: List[ValidPermissions] = Form(...): List validation handled by FastAPI. Form could add description.
    • update_role:
      • id: int = Form(...), organization_id: int = Form(...), permissions: List[ValidPermissions] = Form(...): Similar to create_role.
      • name: str = Form(...): Strong Candidate. Same as in create_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.
  4. 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. A min_length if not None 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 for Form.

Summary of Recommendations:

Apply the Annotated[..., Form(...)] pattern primarily to:

  1. routers/account.py:
    • name in register (non-empty, strip whitespace).
    • password/confirm_password fields in register, reset_password, login, delete_account (at least min_length=1 or min_length=8, description, title).
    • token in reset_password (non-empty, strip whitespace).
  2. routers/role.py:
    • name in create_role and update_role (non-empty, strip whitespace).
  3. routers/user.py:
    • name in update_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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions