第 7 章 field_validator 自定义字段校验
上一章里,我们已经能用嵌套模型描述用户、文章和评论,也能通过 loc 定位嵌套字段错误。
例如:
from pydantic import BaseModel, Field
class UserInfo(BaseModel):
username: str = Field(min_length=2, max_length=20)
email: str
class CommentCreate(BaseModel):
username: str = Field(min_length=2, max_length=20)
content: str = Field(min_length=2, max_length=200)
likes: int = Field(default=0, ge=0)
class ArticleDetail(BaseModel):
title: str = Field(min_length=2, max_length=50)
content: str = Field(min_length=10)
author: UserInfo
comments: list[CommentCreate] = Field(default_factory=list)这些模型已经能处理类型、长度、范围、默认值和嵌套结构。接下来遇到的规则就没法只靠 Field 表达了:用户名要去掉前后空格、不能有中间空格;评论不能全是空白;标题不能带“广告”“推广”这样的词。
它们都只检查单个字段,但需要我们写一点判断逻辑,这正是 field_validator 要做的事。本章只讲单字段规则;要同时检查多个字段,下一章再用 model_validator。
本章学习目标
类型注解和 Field 管不到的单字段业务规则,就交给 field_validator。这章会写 @field_validator("字段名"),在方法里清理前后空格、检查内容,不合规则时抛出 ValueError。你还要能读懂最终的 ValidationError,知道什么时候用 Field,什么时候写校验器。
字段校验器与执行阶段
这一章继续使用文章发布数据。
文章数据里有三类这样的规则:
UserInfo.username
-> 去掉前后空格
-> 不能包含中间空格
CommentCreate.content
-> 去掉前后空格
-> 不能是空白内容
ArticleDetail.title
-> 不能包含简单敏感词,例如“广告”“推广”它们都只看一个字段,不需要检查字段之间的关系,所以适合交给 field_validator。
先看一眼本章新增的能力:
前面学过类型注解、Field 和 ValidationError。这里再加上校验器的执行时机:
before 清理原始输入 → 类型和 Field 规则 → after 检查已校验的值每一步都可以把完整代码复制为本地 .code/main.py,在 .code 目录运行。进入下一步时覆盖这个文件。
单字段规则与用户名校验
Field 通用规则
Field 适合处理声明式规则。
例如:
username: str = Field(min_length=2, max_length=20)
likes: int = Field(default=0, ge=0)这类规则通常包括:
- 最小长度
- 最大长度
- 数值范围
- 默认值
default_factory
这类规则直接写在字段上就够了。
field_validator 自定义规则
有些规则没法只靠 Field 表达。
例如:
用户名不能包含空格
评论内容不能全是空白
标题不能包含某些词这时就要自己写 Python 逻辑。
例如:
@field_validator("username", mode="before")
@classmethod
def validate_username(cls, value: object) -> object:
if not isinstance(value, str):
return value
value = value.strip()
if " " in value:
raise ValueError("用户名不能包含空格")
return valuemode="before" 会让 strip() 在 Field(min_length=2, max_length=20) 之前执行。原始输入不一定是字符串,所以先用 isinstance() 判断。清理后把值交还给 Pydantic:" x " 会变成 "x",再被最小长度规则拦下;如果把清理放到默认的 after 模式,原始的三个字符会先通过长度检查。
先从 UserInfo.username 开始。
用户名要去掉前后空格,中间不能再有空格。
将下面的完整代码保存为 .code/main.py:
from pydantic import BaseModel, Field, ValidationError, field_validator
class UserInfo(BaseModel):
username: str = Field(min_length=2, max_length=20)
email: str
@field_validator("username", mode="before")
@classmethod
def validate_username(cls, value: object) -> object:
if not isinstance(value, str):
return value
value = value.strip()
if " " in value:
raise ValueError("用户名不能包含空格")
return value
data = {
"username": " xiaoming ",
"email": "xiaoming@example.com",
}
user = UserInfo.model_validate(data)
print(user.username)
print(user.email)
short_data = {**data, "username": " x "}
try:
UserInfo.model_validate(short_data)
except ValidationError as e:
print("清理后用户名过短:")
print(e)运行:
uv run python main.pyfrom pydantic import BaseModel, Field, ValidationError, field_validator
class UserInfo(BaseModel):
username: str = Field(min_length=2, max_length=20)
email: str
@field_validator("username", mode="before")
@classmethod
def validate_username(cls, value: object) -> object:
if not isinstance(value, str):
return value
value = value.strip()
if " " in value:
raise ValueError("用户名不能包含空格")
return value
data = {
"username": " xiaoming ",
"email": "xiaoming@example.com",
}
user = UserInfo.model_validate(data)
print(user.username)
print(user.email)
short_data = {**data, "username": " x "}
try:
UserInfo.model_validate(short_data)
except ValidationError as e:
print("清理后用户名过短:")
print(e)可能看到类似输出:
xiaoming
xiaoming@example.com
清理后用户名过短:
1 validation error for UserInfo
username
String should have at least 2 characters校验器先清掉前后空格,再检查中间是否还有空格。通过后要 return value,把处理后的值交回给 Pydantic;所以 " x " 清理成 "x" 后会被 Field(min_length=2) 拦下。如果输入是:
"xiao ming"就会触发:
raise ValueError("用户名不能包含空格")可以把这段逻辑看成:
校验器的模型归属
谁的字段需要自定义校验,就把校验方法写在哪个模型里。
例如:
class UserInfo(BaseModel):
username: str = Field(min_length=2, max_length=20)
email: str
@field_validator("username", mode="before")
@classmethod
def validate_username(cls, value: object) -> object:
...校验器写在 UserInfo 里,因为它检查的是这个模型的 username 字段。
Field 与 field_validator 的分工
可以用下面这张图快速区分两者:
可以直接记成一句话:
Field处理通用规则,field_validator处理需要自己写逻辑的规则。
评论内容校验与清理
字段值的接收与返回
默认的 after 校验器收到已通过基础规则的值,可以这样标注:
def validate_username(cls, value: str) -> str:本章用于清理空格的 before 校验器收到原始输入,建议标注为 object 并先检查类型。不管是哪种模式,通过后都要返回接下来要校验或保存的值:
return value接着用同样的方式处理评论:去掉前后空格,并拒绝清理后为空的内容。
将下面的完整代码保存为 .code/main.py:
from pydantic import BaseModel, Field, field_validator
class CommentCreate(BaseModel):
username: str = Field(min_length=2, max_length=20)
content: str = Field(min_length=2, max_length=200)
likes: int = Field(default=0, ge=0)
@field_validator("content", mode="before")
@classmethod
def validate_content(cls, value: object) -> object:
if not isinstance(value, str):
return value
value = value.strip()
if not value:
raise ValueError("评论内容不能为空")
return value
data = {
"username": "alice",
"content": " 这篇文章很清晰 ",
"likes": 3,
}
comment = CommentCreate.model_validate(data)
print(comment.username)
print(comment.content)
print(comment.likes)运行:
uv run python main.pyfrom pydantic import BaseModel, Field, field_validator
class CommentCreate(BaseModel):
username: str = Field(min_length=2, max_length=20)
content: str = Field(min_length=2, max_length=200)
likes: int = Field(default=0, ge=0)
@field_validator("content", mode="before")
@classmethod
def validate_content(cls, value: object) -> object:
if not isinstance(value, str):
return value
value = value.strip()
if not value:
raise ValueError("评论内容不能为空")
return value
data = {
"username": "alice",
"content": " 这篇文章很清晰 ",
"likes": 3,
}
comment = CommentCreate.model_validate(data)
print(comment.username)
print(comment.content)
print(comment.likes)可能看到类似输出:
alice
这篇文章很清晰
3Field(min_length=2) 能限制长度,却无法判断一串空格是不是有意义。清理并检查内容,需要 field_validator 来处理。
嵌套模型字段校验
ValueError 与 ValidationError
如果字段不符合规则,通常写:
raise ValueError("用户名不能包含空格")外层捕获时,仍然捕获的是:
except ValidationError as e:也就是说:
- 你在字段校验器里抛出的是
ValueError - Pydantic 最终整合出来的是
ValidationError
现在把 UserInfo、CommentCreate 和 ArticleDetail 组合起来,并分别校验用户名、评论内容和文章标题:
UserInfo.username:去掉前后空格,禁止中间空格CommentCreate.content:去掉前后空格,禁止空白评论ArticleDetail.title:禁止包含"广告"、"推广"
将下面的完整代码保存为 .code/main.py:
from pydantic import BaseModel, Field, ValidationError, field_validator
class UserInfo(BaseModel):
username: str = Field(min_length=2, max_length=20)
email: str
@field_validator("username", mode="before")
@classmethod
def validate_username(cls, value: object) -> object:
if not isinstance(value, str):
return value
value = value.strip()
if " " in value:
raise ValueError("用户名不能包含空格")
return value
class CommentCreate(BaseModel):
username: str = Field(min_length=2, max_length=20)
content: str = Field(min_length=2, max_length=200)
likes: int = Field(default=0, ge=0)
@field_validator("content", mode="before")
@classmethod
def validate_content(cls, value: object) -> object:
if not isinstance(value, str):
return value
value = value.strip()
if not value:
raise ValueError("评论内容不能为空")
return value
class ArticleDetail(BaseModel):
title: str = Field(min_length=2, max_length=50)
content: str = Field(min_length=10)
author: UserInfo
comments: list[CommentCreate] = Field(default_factory=list)
@field_validator("title")
@classmethod
def validate_title(cls, value: str) -> str:
blocked_words = ["广告", "推广"]
for word in blocked_words:
if word in value:
raise ValueError("文章标题不能包含敏感词")
return value
bad_data = {
"title": "这是一篇广告文章",
"content": "这是一篇介绍 field_validator 的文章内容",
"author": {
"username": "xiao ming",
"email": "xiaoming@example.com",
},
"comments": [
{
"username": "alice",
"content": " ",
"likes": 3,
}
],
}
try:
article = ArticleDetail.model_validate(bad_data)
print(article)
except ValidationError as e:
for error in e.errors():
print(error)运行:
uv run python main.py输出会包含三条结构化错误。先对照每条错误的 loc 和 msg:
('title',) Value error, 文章标题不能包含敏感词
('author', 'username') Value error, 用户名不能包含空格
('comments', 0, 'content') Value error, 评论内容不能为空上面是从错误字典中摘出的两个键;脚本还会打印 type、input 等信息。
错误发生在不同模型里,最后仍会汇总到外层 ValidationError。loc 会沿着嵌套结构标出位置:
('title',)
('author', 'username')
('comments', 0, 'content')依次表示标题、作者用户名和第 1 条评论内容。每个模型各管自己的字段:
用户字段规则放在 UserInfo
评论字段规则放在 CommentCreate
文章字段规则放在 ArticleDetail校验后字段值
这次换成正确数据,其中用户名和评论内容都带了前后空格,校验器会把它们清理掉。
将下面的完整代码保存为 .code/main.py:
from pydantic import BaseModel, Field, field_validator
class UserInfo(BaseModel):
username: str = Field(min_length=2, max_length=20)
email: str
@field_validator("username", mode="before")
@classmethod
def validate_username(cls, value: object) -> object:
if not isinstance(value, str):
return value
value = value.strip()
if " " in value:
raise ValueError("用户名不能包含空格")
return value
class CommentCreate(BaseModel):
username: str = Field(min_length=2, max_length=20)
content: str = Field(min_length=2, max_length=200)
likes: int = Field(default=0, ge=0)
@field_validator("content", mode="before")
@classmethod
def validate_content(cls, value: object) -> object:
if not isinstance(value, str):
return value
value = value.strip()
if not value:
raise ValueError("评论内容不能为空")
return value
class ArticleDetail(BaseModel):
title: str = Field(min_length=2, max_length=50)
content: str = Field(min_length=10)
author: UserInfo
comments: list[CommentCreate] = Field(default_factory=list)
@field_validator("title")
@classmethod
def validate_title(cls, value: str) -> str:
blocked_words = ["广告", "推广"]
for word in blocked_words:
if word in value:
raise ValueError("文章标题不能包含敏感词")
return value
data = {
"title": "Pydantic 字段校验",
"content": "这是一篇介绍 field_validator 的文章内容",
"author": {
"username": " xiaoming ",
"email": "xiaoming@example.com",
},
"comments": [
{
"username": "alice",
"content": " 这篇文章很清晰 ",
"likes": 3,
}
],
}
article = ArticleDetail.model_validate(data)
print("文章标题:", article.title)
print("作者用户名:", article.author.username)
print("评论内容:", article.comments[0].content)运行:
uv run python main.pyfrom pydantic import BaseModel, Field, field_validator
class UserInfo(BaseModel):
username: str = Field(min_length=2, max_length=20)
email: str
@field_validator("username", mode="before")
@classmethod
def validate_username(cls, value: object) -> object:
if not isinstance(value, str):
return value
value = value.strip()
if " " in value:
raise ValueError("用户名不能包含空格")
return value
class CommentCreate(BaseModel):
username: str = Field(min_length=2, max_length=20)
content: str = Field(min_length=2, max_length=200)
likes: int = Field(default=0, ge=0)
@field_validator("content", mode="before")
@classmethod
def validate_content(cls, value: object) -> object:
if not isinstance(value, str):
return value
value = value.strip()
if not value:
raise ValueError("评论内容不能为空")
return value
class ArticleDetail(BaseModel):
title: str = Field(min_length=2, max_length=50)
content: str = Field(min_length=10)
author: UserInfo
comments: list[CommentCreate] = Field(default_factory=list)
@field_validator("title")
@classmethod
def validate_title(cls, value: str) -> str:
blocked_words = ["广告", "推广"]
for word in blocked_words:
if word in value:
raise ValueError("文章标题不能包含敏感词")
return value
data = {
"title": "Pydantic 字段校验",
"content": "这是一篇介绍 field_validator 的文章内容",
"author": {
"username": " xiaoming ",
"email": "xiaoming@example.com",
},
"comments": [
{
"username": "alice",
"content": " 这篇文章很清晰 ",
"likes": 3,
}
],
}
article = ArticleDetail.model_validate(data)
print("文章标题:", article.title)
print("作者用户名:", article.author.username)
print("评论内容:", article.comments[0].content)可能看到类似输出:
文章标题: Pydantic 字段校验
作者用户名: xiaoming
评论内容: 这篇文章很清晰校验后,用户名和评论内容都没有首尾空格。模型对象保存的是校验器返回的值。
可以再用一张小图单独看“值是怎么被清理并保存下来的”:
文章字段校验综合案例
将下面的完整代码保存为 .code/main.py:
from pydantic import BaseModel, Field, ValidationError, field_validator
class UserInfo(BaseModel):
username: str = Field(min_length=2, max_length=20)
email: str
@field_validator("username", mode="before")
@classmethod
def validate_username(cls, value: object) -> object:
if not isinstance(value, str):
return value
value = value.strip()
if " " in value:
raise ValueError("用户名不能包含空格")
return value
class CommentCreate(BaseModel):
username: str = Field(min_length=2, max_length=20)
content: str = Field(min_length=2, max_length=200)
likes: int = Field(default=0, ge=0)
@field_validator("content", mode="before")
@classmethod
def validate_content(cls, value: object) -> object:
if not isinstance(value, str):
return value
value = value.strip()
if not value:
raise ValueError("评论内容不能为空")
return value
class ArticleDetail(BaseModel):
title: str = Field(min_length=2, max_length=50)
content: str = Field(min_length=10)
author: UserInfo
comments: list[CommentCreate] = Field(default_factory=list)
@field_validator("title")
@classmethod
def validate_title(cls, value: str) -> str:
blocked_words = ["广告", "推广"]
for word in blocked_words:
if word in value:
raise ValueError("文章标题不能包含敏感词")
return value
data = {
"title": "Pydantic 字段校验",
"content": "这是一篇介绍 field_validator 的文章内容",
"author": {
"username": " xiaoming ",
"email": "xiaoming@example.com",
},
"comments": [
{
"username": "alice",
"content": " 这篇文章很清晰 ",
"likes": 3,
},
{
"username": "bob",
"content": " 期待下一章 ",
},
],
}
try:
article = ArticleDetail.model_validate(data)
print("文章标题:", article.title)
print("作者用户名:", article.author.username)
print("第一条评论内容:", article.comments[0].content)
print("第二条评论点赞数:", article.comments[1].likes)
except ValidationError as e:
print("正确数据校验失败")
print(e)
bad_data = {
"title": "这是一篇广告文章",
"content": "这是一篇介绍 field_validator 的文章内容",
"author": {
"username": "xiao ming",
"email": "xiaoming@example.com",
},
"comments": [
{
"username": "alice",
"content": " ",
"likes": 3,
}
],
}
try:
bad_article = ArticleDetail.model_validate(bad_data)
print(bad_article)
except ValidationError as e:
print("错误数据校验失败")
print(e)
for error in e.errors():
location = " -> ".join(str(item) for item in error["loc"])
print("错误位置:", location)
print("错误说明:", error["msg"])
print("输入值:", error["input"])
print("-" * 30)运行:
uv run python main.pyfrom pydantic import BaseModel, Field, ValidationError, field_validator
class UserInfo(BaseModel):
username: str = Field(min_length=2, max_length=20)
email: str
@field_validator("username", mode="before")
@classmethod
def validate_username(cls, value: object) -> object:
if not isinstance(value, str):
return value
value = value.strip()
if " " in value:
raise ValueError("用户名不能包含空格")
return value
class CommentCreate(BaseModel):
username: str = Field(min_length=2, max_length=20)
content: str = Field(min_length=2, max_length=200)
likes: int = Field(default=0, ge=0)
@field_validator("content", mode="before")
@classmethod
def validate_content(cls, value: object) -> object:
if not isinstance(value, str):
return value
value = value.strip()
if not value:
raise ValueError("评论内容不能为空")
return value
class ArticleDetail(BaseModel):
title: str = Field(min_length=2, max_length=50)
content: str = Field(min_length=10)
author: UserInfo
comments: list[CommentCreate] = Field(default_factory=list)
@field_validator("title")
@classmethod
def validate_title(cls, value: str) -> str:
blocked_words = ["广告", "推广"]
for word in blocked_words:
if word in value:
raise ValueError("文章标题不能包含敏感词")
return value
data = {
"title": "Pydantic 字段校验",
"content": "这是一篇介绍 field_validator 的文章内容",
"author": {
"username": " xiaoming ",
"email": "xiaoming@example.com",
},
"comments": [
{
"username": "alice",
"content": " 这篇文章很清晰 ",
"likes": 3,
},
{
"username": "bob",
"content": " 期待下一章 ",
},
],
}
try:
article = ArticleDetail.model_validate(data)
print("文章标题:", article.title)
print("作者用户名:", article.author.username)
print("第一条评论内容:", article.comments[0].content)
print("第二条评论点赞数:", article.comments[1].likes)
except ValidationError as e:
print("正确数据校验失败")
print(e)
bad_data = {
"title": "这是一篇广告文章",
"content": "这是一篇介绍 field_validator 的文章内容",
"author": {
"username": "xiao ming",
"email": "xiaoming@example.com",
},
"comments": [
{
"username": "alice",
"content": " ",
"likes": 3,
}
],
}
try:
bad_article = ArticleDetail.model_validate(bad_data)
print(bad_article)
except ValidationError as e:
print("错误数据校验失败")
print(e)
for error in e.errors():
location = " -> ".join(str(item) for item in error["loc"])
print("错误位置:", location)
print("错误说明:", error["msg"])
print("输入值:", error["input"])
print("-" * 30)字段校验器执行流程
下面这张图把本章 3 个字段校验器的执行位置串起来:
这张图说明:
嵌套模型中,每个模型仍然负责自己的字段校验,最终错误会被汇总起来。
这个案例把本章几件事串在一起:校验器既能清理字段值,也能用 ValueError 拒绝不符合规则的输入;嵌套模型各自校验字段,错误最后汇总到外层 ValidationError。
本章小结
field_validator 处理单个字段的自定义规则。方法通常写在模型类里,配合 @classmethod;通过校验就返回要保存的值,不通过则抛出 ValueError,由 Pydantic 整理成 ValidationError。如果规则要同时看几个字段,下一章再用 model_validator。