(七)把外部输入校验成业务数据
1. 注解不会自动检查一次网络输入
第三章的类型注解帮助阅读和工具分析,第四章的数据类帮助组织字段;它们不会在每次构造时自动执行所有输入规则:
# 项目文件:demos/annotations_validation.py
# 注解和dataclass不阻止错误类型进入;显式校验把错误定位到具体字段。
from webbook.http import Problem
from webbook.validation import BookDraft, validate_book
wrong = BookDraft(12, "false", "基础")
print("仍然构造", wrong.title, type(wrong.finished).__name__)
print("字符串真假", bool("false"))
try:
validate_book({"title": 12, "finished": "false", "tags": "基础"})
except Problem as error:
print("服务器校验", error.status, sorted(error.fields))
print(
"校验后",
validate_book(
{"title": " Python Web ", "finished": False, "tags": ["网络", "网络"]}
),
)第一行仍然构造出了 title 为 12、finished 为字符串的对象。bool("false") 也是 True,因为它是非空字符串,不能用它把 JSON 中错误的文本状态转成 False。
浏览器的 required、maxlength 能在页面上及时提示,但 HTTPX 或另一份页面也能直接发请求。服务器因此要自己执行字段检查。
2. 让错误对应到具体字段
# 项目文件:web_project/src/webbook/validation.py
# dataclass保存已核对的数据,真正的输入检查由函数执行,不依赖注解自动发生。
from dataclasses import dataclass
from .http import Problem, one_value
@dataclass
class BookDraft:
title: str
finished: bool
tags: list[str]
def validate_book(raw):
errors = {}
title, finished, tags = (
raw.get("title"),
raw.get("finished", False),
raw.get("tags", []),
)
if not isinstance(title, str) or not 1 <= len(title.strip()) <= 100:
errors["title"] = "书名应为1至100个字符"
if type(finished) is not bool:
errors["finished"] = "已读状态应为布尔值"
if (
not isinstance(tags, list)
or len(tags) > 10
or any(
not isinstance(tag, str) or not 1 <= len(tag.strip()) <= 30 for tag in tags
)
):
errors["tags"] = "标签应为至多10项的字符串数组,每项1至30个字符"
if errors:
raise Problem(422, "书籍字段需要修改", errors)
return BookDraft(
title.strip(), finished, list(dict.fromkeys(tag.strip() for tag in tags))
)先逐项收集错误,核对通过后才返回 BookDraft。书名与标签两端的空白被移除,重复标签保留首次出现的一项。finished 使用明确的 bool 类型,整数 1 与文本“false”都不会被当作合法 JSON 布尔值。
错误示例返回 422,fields 中分别包含 title、finished、tags,页面可以用它们给对应输入显示说明。下一章的 Pydantic 会用模型字段来组织运行时校验,仍然需要区分类型转换、字段规则与业务冲突。