Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
chriscarrollsmith opened this issue Apr 6, 2025 · 0 comments

Comments

@chriscarrollsmith
Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant