动态过滤与排序参数
构建可复用的查询参数模型,通过验证支持过滤、排序和字段选择。
动态过滤与排序参数 是 CoddyKit 上的免费 FastAPI Backend Development Bootcamp 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 FastAPI Backend Development Bootcamp 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 FastAPI Backend Development Bootcamp 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
Why Dynamic Query Parameters?
Real-world list endpoints rarely return everything. Clients want to filter (only active users), sort (newest first), and select fields (just id and name). Hardcoding every combination explodes your route count.
The clean approach is to model these query parameters as reusable, validated objects that you inject into many endpoints. In this lesson we build:
- A filter model that turns query params into safe constraints
- A sort parser with an allow-list of fields and directions
- A field selection mechanism to trim response payloads
Everything is driven by FastAPI dependencies so it stays DRY and testable.
Collecting Filters with a Dependency Class
A class with __init__ taking Query parameters becomes a reusable dependency. FastAPI reads each parameter from the URL and documents it in OpenAPI automatically.
Use Optional[...] = None so filters are opt-in: a missing param means "don't filter on this column".
from typing import Optional
from fastapi import Query
class UserFilterParams:
def __init__(
self,
status: Optional[str] = Query(None, description="active | inactive"),
min_age: Optional[int] = Query(None, ge=0, le=150),
search: Optional[str] = Query(None, min_length=2, max_length=50),
):
self.status = status
self.min_age = min_age
self.search = search
# Usage:
# @app.get('/users')
# def list_users(filters: UserFilterParams = Depends()):
# ...Validating Filter Values with Enums
Free-text filters like status=foo let bad input through. Constrain them with a str-based Enum: FastAPI rejects anything outside the allowed set and renders a dropdown in the docs.
This is the first line of defence — invalid filter values get a clean 422 instead of leaking into your query layer.
from enum import Enum
from typing import Optional
from fastapi import Query
class UserStatus(str, Enum):
active = "active"
inactive = "inactive"
pending = "pending"
class UserFilterParams:
def __init__(
self,
status: Optional[UserStatus] = Query(None),
min_age: Optional[int] = Query(None, ge=0, le=150),
):
self.status = status
self.min_age = min_ageTurning Filters into Predicates
Keep the HTTP layer separate from the data layer. The dependency only collects and validates; a small helper converts the populated object into actual filter predicates.
Here is a framework-free version you can run, applying filters over plain dicts. The same pattern maps cleanly onto SQLAlchemy .filter() calls later.
USERS = [
{"id": 1, "name": "Ada", "status": "active", "age": 36},
{"id": 2, "name": "Linus", "status": "inactive", "age": 54},
{"id": 3, "name": "Grace", "status": "active", "age": 41},
]
def apply_filters(rows, status=None, min_age=None, search=None):
result = rows
if status is not None:
result = [r for r in result if r["status"] == status]
if min_age is not None:
result = [r for r in result if r["age"] >= min_age]
if search is not None:
result = [r for r in result if search.lower() in r["name"].lower()]
return result
print(apply_filters(USERS, status="active", min_age=40))Parsing a Sort Parameter
A common contract is ?sort=-created_at,name: a comma-separated list where a leading - means descending. Parse it into (field, direction) tuples.
Never trust the client's field names. Validate each field against an allow-list so users can't sort by, or probe, arbitrary columns.
ALLOWED_SORT = {"created_at", "name", "age", "id"}
def parse_sort(sort_param):
parsed = []
for token in sort_param.split(","):
token = token.strip()
if not token:
continue
descending = token.startswith("-")
field = token[1:] if descending else token
if field not in ALLOWED_SORT:
raise ValueError(f"Cannot sort by '{field}'")
parsed.append((field, "desc" if descending else "asc"))
return parsed
print(parse_sort("-created_at,name"))
print(parse_sort("age"))A Reusable Sort Dependency
Wrap the parser in a dependency so every list endpoint shares the same sort contract and validation. Raising HTTPException(422) on a bad field gives clients a precise, machine-readable error.
Passing the allow-list in makes the dependency reusable across resources with different sortable columns.
from typing import Optional
from fastapi import Query, HTTPException
def sort_dependency(allowed: set):
def _parse(sort: Optional[str] = Query(None, example="-created_at,name")):
if not sort:
return []
parsed = []
for token in sort.split(","):
token = token.strip()
if not token:
continue
desc = token.startswith("-")
field = token[1:] if desc else token
if field not in allowed:
raise HTTPException(422, f"Invalid sort field: {field}")
parsed.append((field, "desc" if desc else "asc"))
return parsed
return _parse
# @app.get('/users')
# def list_users(sort=Depends(sort_dependency({'created_at','name'}))):
# ...Applying Multi-Key Sort In Memory
Multiple sort keys must be applied in order. A stable trick: sort by the least significant key first and work backwards, because Python's sorted is stable.
This standalone example mirrors what a database ORDER BY a, b DESC would produce.
ROWS = [
{"name": "Ada", "age": 36},
{"name": "Grace", "age": 36},
{"name": "Linus", "age": 54},
]
def apply_sort(rows, sort_keys):
result = list(rows)
for field, direction in reversed(sort_keys):
result.sort(key=lambda r: r[field], reverse=(direction == "desc"))
return result
ordered = apply_sort(ROWS, [("age", "desc"), ("name", "asc")])
for r in ordered:
print(r)Field Selection (Sparse Fieldsets)
To shrink payloads, support ?fields=id,name. The client picks which keys come back. As always, validate against an allow-list of exposable fields so internal columns (like password_hash) can never be requested.
Selection is a projection step you apply after filtering and sorting, just before serialization.
EXPOSABLE = {"id", "name", "status", "age"}
def select_fields(rows, fields_param):
if not fields_param:
return rows
requested = {f.strip() for f in fields_param.split(",") if f.strip()}
invalid = requested - EXPOSABLE
if invalid:
raise ValueError(f"Unknown fields: {sorted(invalid)}")
return [{k: r[k] for k in requested if k in r} for r in rows]
data = [{"id": 1, "name": "Ada", "status": "active", "age": 36}]
print(select_fields(data, "id,name"))Combining Filter, Sort, Select and Pagination
The pipeline order matters for correctness and efficiency: filter first to reduce the set, then sort, then paginate (slice), and finally select fields on the page you return.
Selecting fields before pagination would still scan everything, and paginating before sorting would return the wrong page.
def list_resource(rows, *, filters, sort_keys, fields, offset, limit,
apply_filters, apply_sort, select_fields):
rows = apply_filters(rows, **filters)
rows = apply_sort(rows, sort_keys)
total = len(rows)
page = rows[offset: offset + limit]
page = select_fields(page, fields)
return {"total": total, "items": page,
"offset": offset, "limit": limit}
# In FastAPI each piece is a Depends(); the route just calls list_resource.Composing Dependencies into One Query Object
Rather than passing four separate dependencies into every route, compose them. A wrapper dependency can return one tidy object holding filters, sort keys, fields, and pagination.
This keeps route signatures short and gives you a single place to evolve the query contract.
from dataclasses import dataclass
from typing import Optional
from fastapi import Depends, Query
@dataclass
class ListQuery:
filters: object
sort: list
fields: Optional[str]
offset: int
limit: int
def list_query(
filters: "UserFilterParams" = Depends(),
sort: list = Depends(sort_dependency({"created_at", "name"})),
fields: Optional[str] = Query(None),
offset: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
) -> ListQuery:
return ListQuery(filters, sort, fields, offset, limit)
# @app.get('/users')
# def list_users(q: ListQuery = Depends(list_query)):
# ...Documenting and Defaulting the Contract
A good query contract is self-documenting and safe by default:
- Give every
Queryadescriptionand anexampleso the OpenAPI docs explain the syntax. - Cap
limitwithle=100so a client can't request a million rows. - Choose a sensible default sort (e.g. newest first) so results are deterministic across pages.
- Reject unknown fields/sort keys with 422 instead of silently ignoring them.
Deterministic ordering is critical: without a stable sort, pagination can repeat or skip rows between requests.
Quick Check: Pipeline Order
You expose GET /products supporting filtering, sorting, pagination, and sparse fieldsets. In what order should these operations be applied to return the correct page efficiently?
Recap
You built a reusable, validated query layer for FastAPI list endpoints:
- Filters as a dependency class with
Optionalparams andEnum/constraint validation. - Sorting parsed from
-field,fieldsyntax against an allow-list, raising 422 on unknown fields. - Field selection (sparse fieldsets) restricted to an exposable allow-list to protect internal columns.
- A composed
ListQuerydependency that keeps route signatures clean.
Remember the pipeline: filter → sort → paginate → select, always with a deterministic default sort so pagination stays consistent. These patterns map directly onto SQLAlchemy queries when you move from in-memory data to a real database.
常见问题解答
「动态过滤与排序参数」课时是免费的吗?
是的 — 「动态过滤与排序参数」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 FastAPI Backend Development Bootcamp 课程的其余内容,请升级到 CoddyKit PRO。 FastAPI Backend Development Bootcamp 课程共包含 4 节课。
「动态过滤与排序参数」这节课中我会学到什么?
构建可复用的查询参数模型,通过验证支持过滤、排序和字段选择。 你通过在浏览器中直接运行的动手代码来练习 FastAPI Backend Development Bootcamp,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 FastAPI Backend Development Bootcamp 需要有经验吗?
无需任何先前经验。CoddyKit 上的 FastAPI Backend Development Bootcamp 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「动态过滤与排序参数」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 FastAPI Backend Development Bootcamp 课中编写并运行代码吗?
能。每节 FastAPI Backend Development Bootcamp 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。