Pydantic 2 的 Optional 字段没传,为什么仍报 Field required
Pydantic 2 区分允许 None 和允许省略。按字段默认值核对必填规则,部分更新用 exclude_unset 保留省略与显式清空的区别。
模型里写了 nickname: Optional[str],传入 {} 仍报 Field required,而传入 {"nickname": None} 却能通过。Pydantic 2 中,这两种输入有不同含义:前者没提供字段,后者提供了值为 None 的字段。
以下示例使用 Python 3.10 及以上支持的 str | None 语法,按 Pydantic 2.10.6 的字段规则整理。Optional[str] 或 str | None 表示值允许为 None,不会自动给字段添加默认值。要允许调用方省略字段,必须显式提供默认值。
先决定省略字段的业务含义
三种声明可以放在一起看:
from pydantic import BaseModel, ValidationError
class Profile(BaseModel):
name: str
nickname: str | None
note: str | None = None
profile = Profile.model_validate({"name": "Ada", "nickname": None})
print(profile.model_dump())
# {'name': 'Ada', 'nickname': None, 'note': None}
try:
Profile.model_validate({"name": "Ada"})
except ValidationError as exc:
print(exc.errors())
name 必须出现且不能为 None;nickname 必须出现,但值可以为 None;note 可以省略,省略后取默认值 None。最后一次验证缺的是 nickname,给 strict=False 不会取消必填规则。
如果接口允许省略昵称,把声明改成 nickname: str | None = None。如果业务要求调用方明确提交昵称或空值,就保留必填声明,修正请求数据。不要为了消掉错误,给所有必填字段都加 None;这会同时改变接口契约与后续代码需要处理的状态。
Field(...) 中的省略号同样表示必填,例如 nickname: str | None = Field(...)。Any 也不能代替默认值:Pydantic 2 的 value: Any 仍然必填,只是接受的值类型更宽。
更新接口要区分“没改”和“清空”
部分更新里,省略字段常表示保持原值,显式 null 则表示清空。可以使用允许省略的更新模型,同时保留这项差异:
from pydantic import BaseModel
class ProfilePatch(BaseModel):
nickname: str | None = None
unchanged = ProfilePatch.model_validate({})
cleared = ProfilePatch.model_validate({"nickname": None})
print(unchanged.model_dump(exclude_unset=True)) # {}
print(cleared.model_dump(exclude_unset=True)) # {'nickname': None}
print(unchanged.model_fields_set) # set()
print(cleared.model_fields_set) # {'nickname'}
保存更新时,只处理实际提交的字段。不能因为模型属性都能读取,就把省略的默认 None 也写回数据库。exclude_none=True 又会删掉显式清空的 None,不适合靠它保留上述更新语义。
模型允许 None 不代表数据库列允许 NULL。若字段禁止清空,应在接口模型或业务验证中明确拒绝,再保证持久化约束一致。
迁移时检查请求和 schema
Pydantic 1 曾让某些 Optional 声明隐含默认 None。迁移到 2 后,要逐项核对原本允许省略的字段,并查看生成 schema 的 required 列表。测试至少包含省略字段、显式 None、正常字符串和不合法类型四种输入;更新接口还要检查省略是否真的保留了原数据。
这与 dataclass 继承中的默认参数顺序不同:这里的模型已经能定义,错误发生在数据验证时。回退字段声明前,也要同步恢复相关客户端或保存逻辑;单独改类型标注,无法撤销已经写入的空值。


