Path Parameters
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against FastAPI 0.142.2 (released 2026-09-30), Pydantic 2.13 and Starlette 1.7, checked on
2026-10-02. All code needs Python 3.10 or newer and was executed on Python 3.13 with TestClient (several routes also
against a real Uvicorn server with curl). A path parameter is a variable segment of the URL path, such as the
42 in /books/42. It identifies which resource a request is about, which is why it is always required. Query
parameters (Query Parameters) refine how the resource is returned, and the
request body (Request Body & Models) carries the data being written.
Declaring and Converting Path Parameters
A path parameter is declared by putting a name in braces in the path template and giving the function a parameter with the same name. FastAPI (via Starlette’s router) extracts the text from the URL and Pydantic converts it to the declared type. The type hint is the whole contract: there is no separate parsing code (see Python Types & Annotated).
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/books/{book_id}")
def read_book(book_id: int) -> dict:
return {"book_id": book_id, "type": type(book_id).__name__}
c = TestClient(app)
print(c.get("/books/42").json())
r = c.get("/books/abc")
print(r.status_code)
print(r.json())
print(c.get("/books/4.2").json()["detail"][0]["msg"])
print(c.get("/openapi.json").json()["paths"]["/books/{book_id}"]["get"]["parameters"])
Source: FastAPI: Path Parameters
The handler receives a Python int, not the string "42":
{"book_id": 42, "type": "int"}
A value that cannot be converted never reaches the function. FastAPI answers 422 (Starlette now labels that status
Unprocessable Content; older releases said Unprocessable Entity) and says exactly where the problem is. The loc
list starts with path, then the parameter name:
{
"detail": [
{
"type": "int_parsing",
"loc": ["path", "book_id"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "abc"
}
]
}
4.2 fails the same way, because int in the default (lax) mode accepts text such as "42" but not a decimal.
In OpenAPI the parameter is listed as {'name': 'book_id', 'in': 'path', 'required': True, 'schema': {'type':
'integer', 'title': 'Book Id'}}; Swagger UI therefore renders a required integer field, and generated clients get a
typed argument.
Two distinct failures are easy to mix up. A path value that is malformed (/books/abc) is a validation error, 422.
A well-formed value for which no record exists (/books/99999) is a business outcome: the handler raises
HTTPException(status_code=404) itself, as in the larger example later on this page and in
Status Codes & Error Handling. 404 from the router itself
means that no route template matched the URL at all.
Which Types Work
Any type that Pydantic can build from a string works, because path values always arrive as text. The common ones:
| Declared type | Accepted text | Rejected with |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
any text without |
(never fails; see the |
|
|
|
|
|
|
|
one of the member values |
|
|
one of the literals |
|
from datetime import date
from typing import Literal
from uuid import UUID
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/authors/{author_id}")
def author(author_id: UUID) -> dict:
return {"id": str(author_id), "type": type(author_id).__name__}
@app.get("/releases/{day}")
def release(day: date) -> dict:
return {"day": day.isoformat()}
@app.get("/flags/{on}")
def flag(on: bool) -> dict:
return {"on": on}
@app.get("/exports/{fmt}")
def export(fmt: Literal["json", "xml"]) -> dict:
return {"fmt": fmt}
c = TestClient(app)
print(c.get("/authors/0b0e8a0e-3b0a-4a6a-9c33-3a1d7a0f5c11").json())
print(c.get("/authors/nope").json()["detail"][0]["msg"])
print(c.get("/releases/2026-10-02").json())
print(c.get("/releases/2026-13-02").json()["detail"][0]["msg"])
for text in ["true", "1", "yes", "on", "maybe"]:
r = c.get(f"/flags/{text}")
print(text, r.status_code, r.json())
print(c.get("/exports/json").json(), c.get("/exports/csv").json()["detail"][0]["msg"])
Source: FastAPI: Path Parameters
The real output, abridged:
{'id': '0b0e8a0e-3b0a-4a6a-9c33-3a1d7a0f5c11', 'type': 'UUID'}
Input should be a valid UUID, invalid character: found `n` at 1
{'day': '2026-10-02'}
Input should be a valid date or datetime, month value is outside expected range of 1-12
true 200 {'on': True}
1 200 {'on': True}
yes 200 {'on': True}
on 200 {'on': True}
maybe 422 {'detail': [{'type': 'bool_parsing', 'loc': ['path', 'on'], 'msg': 'Input should be a valid boolean, unable to interpret input', 'input': 'maybe'}]}
{'fmt': 'json'} Input should be 'json' or 'xml'
Pydantic also offers constrained identifier types. UUID4 from pydantic accepts only version-4 UUIDs, rejects other
versions with UUID version 4 expected, and is documented in OpenAPI as {'type': 'string', 'format': 'uuid4'}. Which
conversions are lax and which are strict is covered in
Pydantic v2 Essentials.
A path parameter is always required and cannot have a default value (see the pitfalls). Structured data such as a Pydantic model belongs in the request body, not in the path.
Route Order Matters
Routes are tried in the order they were declared and the first match wins. A template with a variable segment matches
any text, so a fixed path such as /books/featured must be declared before /books/{book_id}; otherwise "featured"
is captured as the id.
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/books/{book_id}") # declared first: captures "featured" too
def one(book_id: int) -> dict:
return {"book": book_id}
@app.get("/books/featured") # never reached
def featured() -> dict:
return {"featured": True}
@app.get("/authors/featured") # fixed path declared before the variable one: works
def author_featured() -> dict:
return {"featured": True}
@app.get("/authors/{author_id}")
def author(author_id: int) -> dict:
return {"author": author_id}
c = TestClient(app)
r = c.get("/books/featured")
print(r.status_code, r.json()["detail"][0]["type"])
print(c.get("/authors/featured").json())
@app.get("/dup")
def first() -> int:
return 1
@app.get("/dup")
def second() -> int:
return 2
print(c.get("/dup").json())
Source: FastAPI: Path Parameters
/books/featured answers 422 int_parsing (the router picked one and the conversion failed) while
/authors/featured answers {'featured': True}. The same rule means that you cannot redefine a path operation:
declaring GET /dup twice always runs the first function (the output is 1), with no error at startup. The same
applies across routers; routers are consulted in the order of include_router calls.
A subtle variant: /books/{book_id} with book_id: str would happily swallow featured and return it as an id, so
the bug moves from a visible 422 to wrong data. Declaring the fixed paths first, or giving them a different shape
(/books/featured and /books/by-id/{book_id}), avoids both.
Predefined Values with Enum
When only a closed set of values is valid, declare an Enum. Inheriting from str (or, in Python 3.11 and later,
enum.StrEnum) tells OpenAPI that the values are strings. FastAPI validates against the member values and passes the
function the member.
from enum import Enum
from fastapi import FastAPI
from fastapi.testclient import TestClient
class Genre(str, Enum):
fiction = "fiction"
science = "science"
history = "history"
class Format(int, Enum):
paperback = 1
hardcover = 2
app = FastAPI()
@app.get("/genres/{genre}")
def by_genre(genre: Genre) -> dict:
if genre is Genre.science: # compare with an enumeration member
note = "compared with the member"
elif genre.value == "history": # or with the underlying value
note = "compared with .value"
else:
note = "member name is " + genre.name
return {"genre": genre, "note": note} # members are serialized as their value
@app.get("/formats/{fmt}")
def fmt(fmt: Format) -> dict:
return {"fmt": fmt, "name": fmt.name}
c = TestClient(app)
print(c.get("/genres/science").json())
print(c.get("/genres/history").json())
print(c.get("/genres/poetry").json())
print(c.get("/formats/2").json(), c.get("/formats/3").status_code)
schema = c.get("/openapi.json").json()
print(schema["components"]["schemas"]["Genre"])
print(schema["paths"]["/genres/{genre}"]["get"]["parameters"])
Source: FastAPI: Path Parameters
curl -s http://127.0.0.1:8000/genres/poetry
{
"detail": [
{
"type": "enum",
"loc": ["path", "genre"],
"msg": "Input should be 'fiction', 'science' or 'history'",
"input": "poetry",
"ctx": {"expected": "'fiction', 'science' or 'history'"}
}
]
}
The error message lists the valid values, so the client does not need documentation to recover. In OpenAPI the enum
becomes a reusable component, {'type': 'string', 'enum': ['fiction', 'science', 'history'], 'title': 'Genre'}, and the
parameter references it with $ref. Swagger UI shows a drop-down with the allowed values, and generated clients get an
enum type. An int-based enum (Format) accepts the number 2 from the URL (the text is converted first) and rejects
3. Use Literal for a tiny inline set, and an Enum when the set is reused across routes, models and response schemas.
Paths Inside Path Parameters: the :path Converter
A normal path parameter stops at the next /. To capture the rest of the URL, including slashes, use Starlette’s
path converter: put :path after the name in the template.
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/files/{file_path:path}")
def read_file(file_path: str) -> dict:
return {"file_path": file_path}
@app.get("/covers/{name}")
def cover(name: str) -> dict:
return {"name": name}
c = TestClient(app)
print(c.get("/files/covers/2026/dune.png").json())
print(c.get("/files//etc/hosts").json())
print(c.get("/covers/covers/dune.png").status_code)
print(list(c.get("/openapi.json").json()["paths"]))
Source: FastAPI: Path Parameters
/files/covers/2026/dune.png yields covers/2026/dune.png. To receive a path with a leading slash, repeat the slash in
the URL: /files//etc/hosts yields /etc/hosts. The plain str route returns 404 for the same text with slashes,
because the template has no more segments to give them to.
The OpenAPI schema lists the path as /files/{file_path}: the :path suffix is dropped because OpenAPI has no way to
say that a parameter may contain slashes. The route itself works, but the interactive documentation therefore
cannot describe it as a multi-segment value; exercise it with curl and check how your browser or client encodes
slashes (an encoded %2F is decoded before routing and no longer matches a plain str template).
Starlette ships these converters: str (the default), path, int, float and uuid
(Starlette: Routing). FastAPI’s type hint is the better tool for int, float and
UUID, because the hint also produces a 422 explanation and the OpenAPI type. A converter such as \{x:int} only
affects routing: /int/z falls through to 404 instead of 422, and the schema shows an ordinary {x}. The
:path converter is the one that has no hint-only equivalent.
Treat a :path value as untrusted input. Never join it onto a directory with plain string concatenation: ..
segments and absolute paths (the double-slash form above) can escape the intended directory. Resolve it and check that
it stays below the base directory, or look files up by an opaque id instead.
Validation with Path
Path() plays for path parameters the role that Query() plays for query parameters: it adds validation rules and
documentation metadata. Use it inside Annotated, so the parameter keeps its natural signature (see
Annotated).
from typing import Annotated
from fastapi import FastAPI, Path
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/books/{book_id}")
def read_book(
book_id: Annotated[
int,
Path(
title="Book ID",
description="Primary key of the book.",
ge=1,
le=10_000,
examples=[42],
),
],
) -> dict:
return {"book_id": book_id}
@app.get("/shelves/{shelf}/slots/{slot}")
def slot(
shelf: Annotated[str, Path(min_length=2, max_length=4, pattern=r"^[A-Z]+$")],
slot: Annotated[int, Path(gt=0, lt=100, multiple_of=5)],
q: str,
) -> dict:
return {"shelf": shelf, "slot": slot}
@app.get("/ratings/{score}")
def rating(score: Annotated[float, Path(gt=0, le=5)]) -> dict:
return {"score": score}
c = TestClient(app)
print(c.get("/books/0").json()["detail"][0])
print(c.get("/books/10001").json()["detail"][0]["type"])
print(c.get("/books/5").json())
print(c.get("/shelves/ab/slots/7?q=x").json()["detail"])
print(c.get("/shelves/AB/slots/10?q=x").json())
print(c.get("/ratings/0").json()["detail"][0]["msg"], c.get("/ratings/5").status_code, c.get("/ratings/nan").status_code)
print(c.get("/openapi.json").json()["paths"]["/books/{book_id}"]["get"]["parameters"])
The constraint violations carry the failing rule in ctx, which clients can use to build messages:
{
"type": "greater_than_equal",
"loc": ["path", "book_id"],
"msg": "Input should be greater than or equal to 1",
"input": "0",
"ctx": {"ge": 1}
}
Numeric constraints
| Parameter | Meaning | Error type |
|---|---|---|
|
greater than |
|
|
greater than or equal |
|
|
less than |
|
|
less than or equal |
|
|
value must be a multiple of the number |
|
They work for int and float; /ratings/0 fails gt=0, /ratings/5 passes le=5, and /ratings/nan is rejected
with 422 because Pydantic refuses nan and infinities unless allow_inf_nan=True is set. Constraints on a Decimal
can use max_digits and decimal_places.
String constraints
For str parameters use min_length, max_length and pattern (a regular expression; the older name regex is
deprecated). In the example, ab violates the pattern (string_pattern_mismatch) and 7 is not a multiple of 5 in the
same request; both errors are reported together in one 422.
Metadata and OpenAPI effect
The rules appear in the generated schema, so Swagger UI shows them and client generators can enforce them:
{
"name": "book_id",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"maximum": 10000,
"minimum": 1,
"title": "Book ID",
"description": "Primary key of the book.",
"examples": [42]
},
"description": "Primary key of the book."
}
gt/lt become exclusiveMinimum/exclusiveMaximum (OpenAPI 3.1 and JSON Schema 2020-12 use numbers there, not
booleans); multiple_of becomes multipleOf; pattern, min_length and max_length become pattern, minLength
and maxLength.
The full list of parameters accepted by Path() in FastAPI 0.142 (checked by inspecting the function signature) is
below. Most are shared with Query(), Header() and Cookie():
| Parameters | Purpose |
|---|---|
|
Accepted by the signature, but a path parameter cannot have a default; |
|
Documentation shown in Swagger UI and ReDoc. |
|
Numeric and decimal constraints. |
|
String constraints ( |
|
Disable lax conversion; see the pitfall below, where it rejects every value for non-string types. |
|
One or more example values ( |
|
Marks the parameter as deprecated in OpenAPI ( |
|
Controls whether the parameter appears in the generated schema. |
|
Pydantic aliasing; not useful for path parameters, because a template name must match the function parameter and cannot contain |
|
Advanced Pydantic hooks, available for completeness. |
Reusable aliases
Because the constraints live in an Annotated type, they can be named once and reused by every route that takes the same
identifier. The Bookshelf API does this with BookId and AuthorId in the larger example below. The same technique
turns into shared dependency aliases in Dependency Injection.
Parameter Order and the * Trick
Python does not allow a parameter without a default after one that has a default. Older FastAPI code put Path() in
the default value position (book_id: int = Path(ge=1)), which collides with parameters that have no default, such
as a required query parameter. Code of that era solved it by starting the signature with *, which turns every
following parameter into keyword-only so that order stops mattering:
from typing import Annotated
from fastapi import FastAPI, Path
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/old/{book_id}") # code you will meet in older guides
def old(*, book_id: int = Path(ge=1), q: str) -> dict:
return {"book_id": book_id, "q": q}
@app.get("/new/{book_id}") # Annotated: no trick needed
def new(q: str, book_id: Annotated[int, Path(ge=1)]) -> dict:
return {"book_id": book_id, "q": q}
c = TestClient(app)
print(c.get("/old/1?q=a").json(), c.get("/new/1?q=a").json())
try:
exec("def bad(book_id: int = 1, q: str): ...")
except SyntaxError as exc:
print("SyntaxError:", exc.msg)
Both routes return \{'book_id': 1, 'q': 'a'}, and the last line prints SyntaxError: parameter without a default
follows parameter with a default, the Python error that the worked around. With Annotated the default-value slot
is free for real defaults, so is unnecessary; you may still use it on purpose to force keyword-only arguments.
FastAPI does not care about parameter order: it recognizes path parameters by name (the name appears in the path
template), query parameters by their simple type, and bodies by their model type.
Trailing Slashes and Redirects
/books and /books/ are different URLs to a router. FastAPI (through Starlette’s redirect_slashes option, on by
default) bridges the gap with a redirect: when a request does not match but the same path with a slash added or removed
does, the response is 307 Temporary Redirect to that URL.
from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/books")
def list_books() -> dict:
return {"list": True}
@app.get("/authors/")
def list_authors() -> dict:
return {"authors": True}
@app.post("/loans")
def create_loan() -> dict:
return {"created": True}
c = TestClient(app)
r = c.get("/books/", follow_redirects=False)
print(r.status_code, r.headers["location"])
r = c.get("/authors", follow_redirects=False)
print(r.status_code, r.headers["location"])
r = c.get("/books/")
print(r.status_code, r.json(), r.history[0].status_code)
r = c.post("/loans/", follow_redirects=False)
print(r.status_code, r.headers["location"])
print(list(c.get("/openapi.json").json()["paths"]))
strict = TestClient(FastAPI(redirect_slashes=False))
strict.app.get("/books")(list_books)
print(strict.get("/books/").status_code, strict.get("/books").status_code)
# an APIRouter with prefix and "/" route: canonical form is WITH the slash
router = APIRouter(prefix="/shelves")
@router.get("/")
def shelves() -> dict:
return {"shelves": []}
app2 = FastAPI()
app2.include_router(router)
c2 = TestClient(app2)
print(c2.get("/shelves", follow_redirects=False).headers["location"])
print(list(c2.get("/openapi.json").json()["paths"]))
The run printed (testserver is the host name that TestClient uses):
307 http://testserver/books
307 http://testserver/authors/
200 {'list': True} 307
307 http://testserver/loans
['/books', '/authors/', '/loans']
404 200
http://testserver/shelves/
['/shelves/']
What this shows:
-
GET /books/is redirected to/books, andGET /authorsto/authors/. The route you declared is the canonical form and is the only one in OpenAPI. -
The redirect is
307, which preserves the method and body, soPOST /loans/is redirected as aPOSTto/loans. Clients that do not follow redirects (manyfetchconfigurations and some HTTP libraries) must call the canonical URL. A redirect also costs a round trip on every call, so documents should show the canonical form. -
Passing
redirect_slashes=FalsetoFastAPI(…)makes a trailing-slash mismatch an ordinary404. This is the stricter, more REST-like choice for a new API, at the price that sloppy clients break instead of being redirected. -
Behind a reverse proxy the redirect’s
Locationis built from the request’sHostheader and scheme. If the proxy does not forward them, clients can be sent to an internalhttp://address. See Middleware, CORS & Proxies for forwarded headers androot_path. -
A prefix router with a
"/"route (APIRouter(prefix="/shelves")) is canonical with the slash; to expose/shelveswithout a slash, declare the route as""(an empty path) instead of"/". Pick one convention for the whole API and apply it to every router.
RESTful URL Design for the Bookshelf API
Path parameters are the vocabulary of a REST API’s URLs. The conventions that keep them predictable (the REST concepts themselves are introduced in Introduction & Architecture; the same design rules apply on other stacks, see REST APIs for Spring Boot):
-
Nouns, not verbs. The URL names the resource and the HTTP method says the action:
GET /books/42, not/getBook?id=42. -
Plural collection, id for the member.
/booksis the collection,/books/{book_id}one member. -
Nest only to express ownership, and not deeply.
/authors/{author_id}/booksis the books of an author; stop at one or two levels (a deeper chain is usually a sign that the inner resource deserves its own top-level URL). -
Identifiers belong in the path, filters in the query.
GET /books/genre/scienceis acceptable as a sub-collection, butGET /books?genre=science&sort=titleis the normal form for filtering, sorting and paging; see Query Parameters. -
Stable, opaque identifiers. Prefer ids that do not leak ordering or counts (
UUID) for public resources, and decide once whether ids are integers or UUIDs so that thePathconstraint can be a shared alias. -
Methods map to CRUD.
POSTon the collection creates,GETreads,PUTreplaces,PATCHupdates part of a member,DELETEremoves; see Introduction & Architecture for the status code table. -
Fixed words are declared before variable segments, as shown in the route-order section.
APIRouter(prefix=…) and include_router group routes by resource so that each file owns one URL subtree, and the
tags argument groups them in Swagger UI. The larger example puts the pieces together:
from enum import Enum
from typing import Annotated
from fastapi import APIRouter, FastAPI, HTTPException, Path
from fastapi.testclient import TestClient
BookId = Annotated[int, Path(title="Book ID", description="Primary key of a book.", ge=1)]
AuthorId = Annotated[int, Path(title="Author ID", ge=1)]
class Genre(str, Enum):
fiction = "fiction"
science = "science"
history = "history"
BOOKS = {
1: {"id": 1, "title": "Dune", "author_id": 7, "genre": Genre.fiction},
2: {"id": 2, "title": "Cosmos", "author_id": 8, "genre": Genre.science},
}
books = APIRouter(prefix="/books", tags=["books"])
authors = APIRouter(prefix="/authors", tags=["authors"])
@books.get("/")
def list_books() -> list[dict]:
return list(BOOKS.values())
@books.get("/featured") # fixed path: declared BEFORE /{book_id}
def featured_book() -> dict:
return BOOKS[1]
@books.get("/genre/{genre}")
def books_by_genre(genre: Genre) -> list[dict]:
return [b for b in BOOKS.values() if b["genre"] is genre]
@books.get("/{book_id}")
def read_book(book_id: BookId) -> dict:
if book_id not in BOOKS:
raise HTTPException(status_code=404, detail="Book not found")
return BOOKS[book_id]
@authors.get("/{author_id}/books")
def books_of_author(author_id: AuthorId) -> list[dict]:
return [b for b in BOOKS.values() if b["author_id"] == author_id]
@authors.get("/{author_id}/books/{book_id}")
def book_of_author(author_id: AuthorId, book_id: BookId) -> dict:
book = BOOKS.get(book_id)
if book is None or book["author_id"] != author_id:
raise HTTPException(status_code=404, detail="Book not found for this author")
return book
app = FastAPI(title="Bookshelf API")
app.include_router(books)
app.include_router(authors)
c = TestClient(app)
for url in ["/books/featured", "/books/2", "/books/99", "/books/0", "/books/abc",
"/books/genre/science", "/books/genre/poetry", "/authors/7/books", "/authors/7/books/2"]:
r = c.get(url)
print(url, r.status_code, r.json())
Saved as main.py, run it with fastapi dev and call it (outputs are from a real Uvicorn run of this module):
curl -i http://127.0.0.1:8000/books/2
curl -i http://127.0.0.1:8000/books/99
curl -i http://127.0.0.1:8000/books/abc
curl -i http://127.0.0.1:8000/authors/7/books/2
{"id": 2, "title": "Cosmos", "author_id": 8, "genre": "science"}
/books/99 answers 404 with \{"detail": "Book not found"}; /books/abc answers 422 Unprocessable Content with the
int_parsing detail shown earlier; /books/0 answers 422 with greater_than_equal (the BookId alias applies
everywhere it is used, including the nested route); and /authors/7/books/2 answers 404 with \{"detail": "Book not
found for this author"}, because book 2 belongs to author 8. The generated OpenAPI paths are /books/,
/books/featured, /books/genre/{genre}, /books/{book_id}, /authors/{author_id}/books and
/authors/{author_id}/books/{book_id}; the constraint and the description of BookId appear on every one of them
(minimum: 1, title: "Book ID").
The nested route shows the typical pattern for ownership checks: both ids are validated by the path, then the handler
verifies that the book belongs to the author and answers 404 rather than leaking that the book exists elsewhere.
Pitfalls
-
Variable before fixed.
/books/{book_id}declared above/books/featuredcapturesfeatured. Withbook_id: intthe symptom is a puzzling422; withstrit is silently wrong data. -
Pathwith a default. Declaringbook_id: Annotated[int, Path(ge=1)] = 1raisesAssertionError: Path parameters cannot have default valueswhen the route is declared (at import time), because a path parameter is required by construction. A default is meaningful only on query, header and cookie parameters. -
Name mismatch. The function parameter must have exactly the name used in the template. A parameter whose name is not in the template is treated as a query parameter: with the template
/m/{id}and a parameterbook_id: int,GET /m/1answers422with"loc": ["query", "book_id"]and"type": "missing". -
strict=Trueon non-string types. Path values are always text, soAnnotated[int, Path(strict=True)]rejects every request withint_type. Leave strictness off for path parameters (or use astr-based type with a pattern). -
Slashes in
strparameters. A plainstrparameter never contains/. An id likea/bsent asa%2Fbis decoded by the server before routing and yields404; use:pathdeliberately, or use ids that cannot contain slashes. -
Treating the
:pathvalue as a filesystem path. See the path-traversal note above. -
Trailing slash inconsistency. Mixed conventions produce redirects, and
307plus relativeLocationhandling behind proxies can send clients to internal addresses; choose one form and setredirect_slashesdeliberately. -
Assuming OpenAPI knows about
:pathor converters. They are routing-only; the schema reports a plain string. -
Using
Path(…)withEllipsis. In older codePath(…)marked the parameter required. Path parameters are always required and Pydantic v2 reads…as "no default" anyway, so a barePath()is the current spelling. -
Raising
404for malformed ids. Let validation answer422for syntax errors and reserve404for well-formed ids that match nothing. -
Sensitive data in the URL. Paths end up in access logs, proxies and browser history. Do not put tokens, e-mail addresses or other personal data in a path parameter; send them in a header or body.
Where Older Guides Are Outdated, and What Changed Recently
-
Path(…)as a default value and thetrick.* Guides from before FastAPI 0.95.0 (2023-03-18) writeitem_id: int = Path(…, ge=1)and begin the signature with.Annotatedis supported and recommended since 0.95.0, needs no, and drops the…. -
Pydantic v1 names. In v1-era material, numeric validation was often written with
conintand string validation withconstr(regex=…); the current spelling isAnnotated[int, Path(ge=1)]orField, andPath(pattern=…)replacesPath(regex=…). v1 support was removed in FastAPI 0.126.0 (2025-12-20) and thepydantic.v1fallback in 0.128.0 (2025-12-27). The422error payloads changed with v2 as well: thetypeis nowint_parsingorgreater_than_equalinstead oftype_error.integerorvalue_error.number.not_ge. -
Examples. The single
example=argument is deprecated in favour ofexamples=[…], and OpenAPI 3.1 support arrived in 0.99.0 (2023-06-30);openapi_examples=appeared in 0.103.0 (2023-08-26). -
Optionaland thetypingimports. Current style isint | Noneand built-in generics; Python 3.10 is the minimum since 3.9 was dropped in 0.129.0. -
Trailing slashes in tutorials. Book- and blog-style code routinely declares both
/booksand/books/or relies on the redirect without saying so. The behaviour described above is Starlette’s, applies to routers withprefix, and can be turned off withredirect_slashes=False. -
Starlette 1.x. FastAPI supports Starlette 1.0 and later since 0.133.0 (2026-02-24); path templates and converters behave as before.
-
Status text. The
422reason phrase isUnprocessable Contentin current releases; clients should rely on the numeric code, not the phrase.