FastAPI Interactive Docs

Query Parameters and String Validation

Query Parameters and String Validations in FastAPI

In the Query Param article, we met query parameters and sprinkled in a quick Query(min_length=3) near the end. We stopped there on purpose. Query can do a lot more than guard a length, so it deserves its own spotlight. Let's dig in, with a focus on strings.

Quick recap of the setup. We have a search endpoint, and q is our query parameter.

from typing import Optionalfrom fastapi import FastAPIapp = FastAPI()@app.get("/blog")async def search_blogs(q: Optional[str] = None):    return {"q": q}

q is optional because its default is None. Nothing new yet. Now let's add some rules.

Bringing in Query and Annotated

Remember Annotated from the Path Param article? Same tool, same idea. We wrap our type with it and slot Query inside to attach validation.

from typing import Annotated, Optionalfrom fastapi import FastAPI, Queryapp = FastAPI()@app.get("/blog")async def search_blogs(q: Annotated[Optional[str], Query(max_length=50)] = None):    return {"q": q}

Let's review:

  1. The type is still Optional[str], so q can be a string or nothing.

  2. The default is still None, so it stays optional.

  3. Query(max_length=50) is the new bit. If the client does send q, it cannot be longer than 50 characters.

Send a 60-character search and FastAPI rejects it with a clean error. No manual len() checks on your side.

min, max, and patterns

You can stack string rules together.

@app.get("/blog")async def search_blogs(    q: Annotated[Optional[str], Query(min_length=3, max_length=50)] = None,):    return {"q": q}

Now q must be between 3 and 50 characters. Search for hi and FastAPI says no.

Need tighter control over the shape of the string? Query accepts a regular expression through pattern.

@app.get("/blog")async def search_blogs(    q: Annotated[Optional[str], Query(pattern="^fixedquery$")] = None,):    return {"q": q}

That pattern means the value must be exactly fixedquery, nothing before and nothing after. If regular expressions make your head spin, do not worry. You can build plenty without them and pick them up the day you actually need them. 🙂

Default values and required

A default of None makes a parameter optional, but the default can be any value you like.

@app.get("/blog")async def search_blogs(q: Annotated[str, Query(min_length=3)] = "fastapi"):    return {"q": q}

Skip q in the URL and it falls back to "fastapi".

Want to force the client to always send q? Just drop the default value entirely.

@app.get("/blog")async def search_blogs(q: Annotated[str, Query(min_length=3)]):    return {"q": q}

No default means required. Hit /blog with nothing and FastAPI complains that q is missing.

Accepting multiple values

Here is a fun one. What if you want q to show up several times, like /blog?q=python&q=fastapi? Annotate it as a list.

@app.get("/blog")async def search_blogs(q: Annotated[Optional[list[str]], Query()] = None):    return {"q": q}

A URL like /blog?q=python&q=fastapi now gives you {"q": ["python", "fastapi"]}. FastAPI gathered both values into a Python list for you.

Let's review one subtle thing:

  1. We had to use Query() explicitly here. Without it, FastAPI would assume a list means a request body (remember request bodies from the last article?), not a query parameter.

  2. list[str] checks that each item is a string. A plain list would skip that check.

A handy alias

Sometimes the name you want in the URL is not a valid Python variable name. Say the client expects item-query, but Python will not let you name a variable with a dash in it. alias solves this.

@app.get("/blog")async def search_blogs(q: Annotated[Optional[str], Query(alias="item-query")] = None):    return {"q": q}

Now the URL uses ?item-query=... while your code happily keeps calling it q.

There is more you can hand to Query, like title and description to enrich your auto-generated docs, or deprecated=True to flag an old parameter on its way out. Same pattern every time, just more keyword arguments.

Gotcha: When you use Query inside Annotated, do not pass default to it. Set the default the normal Python way, on the function parameter. So write q: Annotated[str, Query()] = "fastapi", not Query(default="fastapi"). Otherwise FastAPI cannot tell which default you actually meant, and that is just asking for confusion. 😁

Practice Lab: