Parameter Validation

Difficulty: Beginner

FastAPI provides powerful parameter validation through the Query and Path functions. These let you add constraints beyond simple type checking, including string length limits, numeric ranges, regex patterns, and more. This validation happens automatically before your handler runs, so you never receive invalid data.

For string parameters, you can use min_length and max_length to constrain the string length. For example, Query(min_length=3, max_length=50) ensures the string is between 3 and 50 characters. You can also use pattern (formerly regex) to validate against a regular expression.

For numeric parameters (int, float), you can use gt (greater than), ge (greater than or equal), lt (less than), and le (less than or equal). For example, Query(ge=1, le=100) ensures the number is between 1 and 100 inclusive.

The modern way to apply validation in FastAPI uses Python's Annotated type hint. Instead of writing q: str = Query(min_length=3), you write q: Annotated[str, Query(min_length=3)]. The Annotated approach is preferred because it keeps the type information separate from the default value, making it clearer and more composable.

You can also add metadata to parameters for documentation purposes. The title, description, and example parameters in Query() appear in the auto-generated Swagger documentation. This makes your API self-documenting. The alias parameter lets the query parameter have a different name in the URL than in the function signature, which is useful when query parameter names conflict with Python keywords.

Query() and Path() can also mark parameters as deprecated using deprecated=True. This adds a visual indicator in the documentation that the parameter is deprecated, while still accepting it for backward compatibility. The include_in_schema=False parameter hides a parameter from the documentation entirely.

Validation error responses in FastAPI are structured JSON objects that include the location of the error (query, path, body), the invalid value, and a human-readable message. The response uses the standard 422 status code and follows a consistent format that clients can parse programmatically.

Code examples

String Validation

from typing import Annotated
from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/search")
def search(
    q: Annotated[str, Query(
        min_length=2,
        max_length=100,
        title="Search query",
        description="The search term to filter results",
        examples=["fastapi tutorial"],
    )],
):
    return {"query": q}

# GET /search?q=a       -> 422 Error (min_length=2)
# GET /search?q=fast    -> {"query": "fast"}

@app.get("/users")
def search_users(
    username: Annotated[str | None, Query(
        min_length=3,
        max_length=20,
        pattern="^[a-zA-Z0-9_]+
quot;, )] = None, ): return {"username": username} # GET /users?username=ab -> 422 (too short) # GET /users?username=hello@world -> 422 (invalid chars) # GET /users?username=alice_99 -> {"username": "alice_99"}

Annotated[type, Query(...)] is the modern syntax for validated query parameters. min_length and max_length constrain string length. The pattern parameter validates against a regex. Title and description appear in the Swagger docs.

Numeric Validation

from typing import Annotated
from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/items")
def list_items(
    page: Annotated[int, Query(ge=1, description="Page number")] = 1,
    per_page: Annotated[int, Query(ge=1, le=100)] = 20,
    min_price: Annotated[float | None, Query(gt=0)] = None,
    max_price: Annotated[float | None, Query(gt=0)] = None,
):
    return {
        "page": page,
        "per_page": per_page,
        "min_price": min_price,
        "max_price": max_price,
    }

# GET /items?page=0       -> 422 (page must be >= 1)
# GET /items?per_page=200  -> 422 (per_page must be <= 100)
# GET /items?min_price=-5  -> 422 (must be > 0)
# GET /items?page=2&per_page=50 -> valid

ge (>=), gt (>), le (<=), lt (<) constrain numeric values. Combining ge=1 with le=100 creates a range. These constraints are documented automatically in the OpenAPI schema.

Advanced Query Options

from typing import Annotated
from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/items")
def list_items(
    # Alias: URL uses 'item-query', code uses 'item_query'
    item_query: Annotated[str | None, Query(alias="item-query")] = None,

    # Deprecated parameter (still works but shown as deprecated)
    old_filter: Annotated[str | None, Query(deprecated=True)] = None,

    # Hidden from docs
    internal_key: Annotated[str | None, Query(include_in_schema=False)] = None,
):
    return {
        "query": item_query,
        "old_filter": old_filter,
    }

# GET /items?item-query=test  -> works (alias maps to item_query)
# GET /items?item_query=test  -> does NOT work (alias overrides name)
# old_filter appears strikethrough in docs
# internal_key is hidden from docs entirely

Alias maps a URL parameter name to a different Python variable name. Deprecated marks a parameter as deprecated in docs. include_in_schema=False hides it from the OpenAPI documentation entirely.

Key points

Concepts covered

Query Validation, min_length, max_length, regex, ge, le, Annotated