Skip to content

Polishing Inputs: Advanced Validation and Metadata

Polishing Inputs: Advanced Validation and Metadata

Section titled “Polishing Inputs: Advanced Validation and Metadata”

In previous chapters, we relied on basic Python types like str and int to validate data. However, real-world applications require stricter rules. A username might need to be at least 3 characters long, or a product ID must be a positive number. In this chapter, we will unlock the power of Query, Path, and Body to enforce these constraints and enrich our API documentation with metadata.

FastAPI allows you to declare additional validation for parameters using the Query function. To use it, we will leverage the modern Python Annotated syntax.

Imagine a search endpoint where the query string q is optional but, if provided, must be no longer than 50 characters and no shorter than 3 characters.

from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(
q: Annotated[str | None, Query(min_length=3, max_length=50)] = None
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results

We can even enforce regular expressions using the pattern parameter. For example, to ensure a string matches a specific format like a fixed-size code:

# Ensures 'q' is exactly 3 digits if provided
q: Annotated[str | None, Query(pattern="^\\d{3}$")] = None

Just as Query handles query parameters, Path handles path parameters. Since path parameters are always required (they are part of the URL route), we often use Path to validate numeric ranges.

Common validators include:

  • gt: Greater than
  • ge: Greater than or equal to
  • lt: Less than
  • le: Less than or equal to
from fastapi import Path
@app.get("/items/{item_id}")
async def read_items(
item_id: Annotated[int, Path(title="The ID of the item", ge=1)],
q: Annotated[str | None, Query(alias="item-query")] = None,
):
return {"item_id": item_id, "q": q}

In this example, if a user tries to access /items/0 or /items/-5, FastAPI will automatically reject the request with a validation error because item_id must be greater than or equal to 1.

Sometimes you want to pass a single value in the request body (like a simple string) rather than a full JSON object defined by a Pydantic model. By default, FastAPI assumes simple types are Query parameters. To instruct FastAPI to read them from the JSON body, we use Body.

from fastapi import Body
@app.put("/items/{item_id}")
async def update_item(
item_id: int,
item: Item, # A Pydantic model
importance: Annotated[int, Body()] # A single value from the body
):
return {"item_id": item_id, "item": item, "importance": importance}

FastAPI will expect a JSON body that looks like this:

{
"item": {
"name": "Foo",
"price": 45.2
},
"importance": 5
}

One of FastAPI’s best features is the automatic documentation. Using Query, Path, and Body, you can add metadata that appears directly in the Swagger UI and ReDoc.

  • title: A concise title for the parameter.
  • description: A detailed explanation of what the parameter does. Markdown is supported!
  • alias: Useful if the parameter name in the URL needs to be different from your Python variable (e.g., item-query vs item_query).
  • deprecated: Set to True to mark a parameter as deprecated in the docs without breaking the API.
q: Annotated[
str | None,
Query(
title="Query String",
description="Query string for the items to search in the database that have a good match",
min_length=3,
deprecated=True
)
] = None

By adding these details, you turn your code into self-documenting specifications, making life easier for frontend developers and API consumers.