什么是schema?

Schema(模式)是一个描述数据结构和规则的定义,通常以结构化格式( JSON 或 XML)表示。它指定数据的字段、类型、约束和关系,常用于数据验证(检查数据是否满足规则)、文档生成(为 API 或数据库提供清晰的结构说明)以及数据交换(在系统间传递数据时schema 确保一致性)。它定义了一段数据应该“长什么样”,包含哪些字段,每个字段的类型是什么,以及它们之间可能存在哪些关系或约束。

Schema 像一张表格的说明书,规定了表格有哪些列、每列的数据类型和限制(如“年龄必须是正整数”)。任何数据填入表格前,都要对照说明书检查是否合规。

什么是Pydantic?

Pydantic 是一个功能强大的 Python 数据验证库,它通过 Python 类型注解实现快速数据验证和转换,可以强制校验传入数据是否符合预定义的类型和规则,如果数据不符合要求,它会自动抛出详细的错误信息,不仅支持数据验证,还能将输入数据(如 JSON 字符串)解析成符合预期定义的数据模型。

Pydantic 是 FastAPI 等流行 Web 框架的核心组成部分,被广泛用于请求参数体验证和响应序列化,在LangChain中也被广泛使用,因为看不懂LangChain中跟Pydantic相关的代码,所以特地学习一下这个库。

Pydantic 的主要功能:

  • 声明性数据验证:通过声明性的方式定义数据模型,指定每个字段的数据类型和验证规则。
  • 数据转换:可以将输入数据转换为 Python 对象,并根据数据模型进行类型检查和转换。
  • 错误报告:当验证失败时,Pydantic 提供清晰的错误报告,帮助快速找到问题并进行修复。
  • 数据文档生成:可以使用 Pydantic 自动生成数据模型的文档,包括字段的说明和验证规则。
  • 与 Python 类型系统集成:Pydantic 与 Python 类型系统无缝集成,可以轻松将数据模型用于函数参数和返回值。

Pydantic 的数据验证是自动进行的,当创建数据模型实例时(实例化对象),输入数据会自动根据定义的数据类型进行验证和转换。如果验证失败,Pydantic 会抛出详细的 ValidationError 异常。错误信息会包含:具体是哪个字段(属性)验证失败、失败的原因、期望的数据类型、实际接收到的数据类型。


Github项目地址

官方文档

Pydantic 的基石:BaseModel

Pydantic 的核心是 BaseModel自定义类通过继承 BaseModel 来定义自己的数据模型,这些数据模型本质上是带有数据验证功能的 Python 类。

BaseModel 是 Pydantic 的起点,可以把它想象成一个特殊的 Python 类,但它比普通的类更强大,因为它自带了数据验证、数据解析和类型转换的能力。当创建一个继承自 BaseModel 的类时,实际上就是在定义一个数据模型(Data Model),告诉 Pydantic 我的数据应该长什么样,每个字段是什么类型,以及有哪些验证规则。

默认情况下,Pydantic中定义的所有属性都要传值,如果某一个属性没有传值,就会报错,除非这个属性有默认值,那就可以不用传。


继承 BaseModel 的自定义类不需要手动写构造方法

这是 Pydantic 强大而简洁的体现,也是它与传统 Python 类在使用上的一个显著区别。

为什么继承 BaseModel 的自定义类不需要写构造方法?核心原因在于Pydantic 的 BaseModel 在幕后自动完成了构造、验证和解析的工作。当自定义一个继承自 BaseModel 的类时,虽然没有显式地写 __init__() ,但 Pydantic 在内部做了这样一件事:它会根据定义的类属性(name, price, is_available等)和它们的数据类型提示,自动生成一个 __init__() 方法。这个自动生成的 __init__() 方法会接收与类属性同名的参数,然后在自动生成的 __init__() 内部,Pydantic 会自动执行数据验证和类型转换逻辑。

总结来说,Pydantic 通过 BaseModel 及其内部机制,将数据模型的定义、构造、验证和转换过程高度自动化,只需要声明数据的形态,Pydantic 负责让数据符合这个形态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 概念性展示Pydantic内部运作流程,并非内部具体实现
class Product(BaseModel):
name: str
price: float
is_available: bool = True

# Pydantic 内部的__init__()自动做了类似这样的事情:
def __init__(self, name: str, price: float, is_available: bool = True):
# 1. 接收传入的参数
# 2. 对每个参数进行类型验证 (例如,检查 name 是否是 str, price 是否是 float)
# 3. 对参数进行必要的类型转换 (例如,如果 price 传入 "19.99",转换为 19.99)
# 4. 应用在 Field() 或 validator 中定义的额外验证规则
# 5. 如果验证失败,抛出 ValidationError
# 6. 如果验证成功,将这些值赋值给实例属性

Pydantic 主要对类属性(字段)进行验证,确保输入数据符合定义的类型和自定义验证规则。函数通常不直接参与 Pydantic 的验证流程(除非方法被明确用于验证逻辑)。可以正常在继承 BaseModel 的自定义类中定义普通方法、类方法或静态方法来处理与该数据模型相关的业务逻辑,或者定义计算属性@property(如 full_name)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pydantic import BaseModel
from datetime import date

class Person(BaseModel):
first_name: str
last_name: str
birth_date: date

# 普通方法:访问实例属性
def get_greeting(self) -> str:
return f"Hello, my name is {self.first_name} {self.last_name}."

# 计算属性:像属性一样进行访问,但实际是计算出来的
@property
def age(self) -> int:
today = date.today()
return today.year - self.birth_date.year - \
((today.month, today.day) < (self.birth_date.month, self.birth_date.day))

# 实例化时Pydantic自动进行构造和验证
p = Person(first_name="Alice", last_name="Smith", birth_date=date(1990, 1, 15))

print(p.get_greeting()) # 调用普通方法
print(f"I am {p.age} years old.") # 访问计算属性

在传统的 Python 编程中,经常使用构造函数或类方法来定义类的数据结构,在实例化时可以随意传入错误类型,Python 解释器在运行时不会主动检查传入的数据类型是否正确。如果传入了错误类型的数据,可能会导致程序运行崩溃。BaseModel 就是用来解决这个问题的。继承BaseModel定义一个类时,这个类就拥有了 Pydantic 赋予的能力:

  • 强制类型检查和验证: 尝试创建一个类的实例时(如下面的MyPydanticData),Pydantic 会检查传入的 name 是不是字符串,value 是不是整数,如果不符合要求,会立即抛出 ValidationError,而非在后续代码执行时才报错(在数据进入业务逻辑之前)。

  • 自动数据解析和转换: Pydantic 非常智能。如果传入了一个数字字符串给 value: int(例如 "123"),Pydantic 会自动尝试将其转换为整数 123。这在处理从外部(比如 JSON API)获取的数据时非常有用。

  • 提供方便的数据操作方法: BaseModel 实例有很多内置的实用方法,比如方便地转换为字典 (model_dump()) 或 JSON 字符串 (model_dump_json())。

  • 清晰的数据结构定义: 通过类型提示,数据结构一目了然,提高了代码的可读性和可维护性。

  • 在pydantic实例化时可以传入数据模型没有的额外参数,实例化时不会报错,但是pydantic会忽略掉这些参数,传入了也没用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# 传统方法
class MyData:
def __init__(self, name: str, value: int):
self.name = name
self.value = value

data = MyData("example", 123)
# data_error = MyData(123, "example") # 在运行时才可能报错,或者不会报错但导致后续逻辑出错

# Pydantic
from pydantic import BaseModel

class MyPydanticData(BaseModel): # 继承BaseModel
name: str
value: int

# 示例代码
from pydantic import BaseModel
from typing import Optional

# 定义一个 User 模型
class User(BaseModel):
name: str
age: int
city: Optional[str] = None # 表示数据类型是可选字符串,str | None

print("--- 1. 创建有效实例 ---")
# 1. 创建有效实例
user1 = User(name="张三", age=30)
print(f"用户1: {user1}") # 输出:用户1: name='张三' age=30 city=None
print(f"姓名: {user1.name}, 年龄: {user1.age}, 城市: {user1.city}") # 输出:姓名: 张三, 年龄: # 30, 城市: None

user2 = User(name="李四", age=25, city="北京")
print(f"用户2: {user2}") # 输出:用户2: name='李四' age=25 city='北京'
print(f"姓名: {user2.name}, 年龄: {user2.age}, 城市: {user2.city}") # 输出:姓名: 李四, 年龄: # 25, 城市: 北京

print("\n--- 2. 尝试传入错误类型的数据 ---")
# 2. 尝试传入错误类型的数据
try:
# 这里 age 传入了字符串 "35",Pydantic 会尝试转换
user_with_str_age = User(name="王五", age="35")
print(f"成功转换并创建用户: {user_with_str_age}") # 输出:成功转换并创建用户: name='王五' # age=35 city=None
print(f"age 的类型是: {type(user_with_str_age.age)}") # 输出:age 的类型是: <class 'int'>
except Exception as e:
print(f"创建失败 (age类型错误): {e}")

try:
# 这里 age 传入了无法转换为整数的字符串 "abc"
user_with_invalid_age = User(name="赵六", age="abc") # 输出:创建失败 (age无法转换): 1 # validation error for User age
except Exception as e:
print(f"创建失败 (age无法转换): {e}")


print("\n--- 3. 尝试缺少必需字段 ---")
# 3. 尝试缺少必需字段
try:
user_missing_name = User(age=40) # name 是必需字段
except Exception as e:
print(f"创建失败 (缺少 name 字段): {e}")

print("\n--- 4. 将模型转换为字典或 JSON ---")
# 4. 将模型转换为字典或 JSON
user3 = User(name="钱七", age=22, city="上海")
user_dict = user3.model_dump() # Pydantic v2.0+ 使用 model_dump()
user_json = user3.model_dump_json() # Pydantic v2.0+ 使用 model_dump_json()

print(f"用户3 (字典形式): {user_dict}") # 用户3 (字典形式): {'name': '钱七', 'age': 22, 'city': '上海'}
print(f"用户3 (JSON 字符串形式): {user_json}") # 用户3 (JSON 字符串形式): {"name":"钱 七","age":22,"city":"上海"}

Field函数

Field 是 Pydantic 提供的一个函数/标记(也可以称为工厂函数),用于在数据模型属性(字段)上指定更丰富的元信息和校验约束。例如:默认值、校验范围、描述文档、示例、别名等。如果想对某个字段添加额外的约束、元数据或验证规则,就需要用到Field 了。可以使用它实现下面功能:

  • 添加额外的验证规则: 增强字段的定义和行为。例如,设置字符串的最小/最大长度,数字的范围,正则表达式匹配等,

  • 设置默认值: 除了直接在属性类型提示后赋值外,Field 也提供了设置默认值的方式,尤其是字段需要默认值且字段本身也可以为 None 时(例如 Optional[str] )。

  • 定义字段的元数据: 添加字段描述、别名、JSON schema 定制等。例如,为字段添加描述信息、标题、示例值等,这对于生成 API 文档(如 OpenAPI/Swagger)非常有用。

  • 处理字段别名: 当传入数据的字段名与模型中定义的字段名不一致时,可以使用别名进行映射。

Field函数常用参数:

  • default:设置字段的默认值,未提供指定值时使用默认值。
  • default_factory:提供生成默认值的可调用对象,适合可变类型。
  • alias:字段别名,指定序列化/反序列化时的别名(如JSON键)。
  • title:字段的人类可读标题,用于文档或模式。
  • const:强制字段为特定常量值(需设置default)。
  • description:字段描述,用于文档。
  • gt:确保数值大于指定值(用于数字字段),Greater Than –> 大于。
  • ge:确保数值大于等于指定值,Greater Than or Equal To –> 大于等于
  • lt:确保数值小于指定值,Less Than –> 小于
  • le:确保数值小于等于指定值,Less Than or Equal To –> 小于等于
  • multiple_of:确保数值是指定值的倍数。
  • max_length:指定字符串或列表的最大长度。
  • pattern:强制字符串匹配指定的正则表达式,Pydantic V1中用的是regex,V2中换成了pattern
  • exclude:序列化时排除字段(例如model_dump())。
  • repr:指定该字段是否应在__repr__方法中显示。若设置为False,则在生成类的__repr__输出时将跳过该字段的显示。
  • validate_default决定是否验证默认值,用于在模型定义阶段验证默认值是否符合约束条件,如果参数值为True,即使字段用了默认值,也要像用户显式传值一样走验证逻辑,用于默认值可能非法、希望也走验证逻辑的情况,若默认值不符合字段的验证规则,则会引发错误。
  • frozen:初始化后禁止修改字段(不可变)。
  • allow_inf_nan:允许或禁止数字字段使用inf、-inf和nan。
  • example:为字段提供示例值,用于模式生成。
  • min_length:指定字符串或列表的最小长度。
  • pattern:正则表达式匹配。
  • exclude:是否排除在序列化中。

Field 函数通常在模型内部作为字段的默认值使用,它会覆盖简单的类型提示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# default参数
from pydantic import BaseModel, Field

class User(BaseModel):
name: str = Field(default="匿名")

user = User()
print(user.name) # 输出: 匿名
-----------------------------------------------------------------------------------------
# default_factory参数
from pydantic import BaseModel, Field
from uuid import uuid4

class User(BaseModel):
id: str = Field(default_factory=lambda: str(uuid4()))

user = User()
print(user.id) # 输出: 唯一UUID字符串

# 生成空列表
class User(BaseModel):
tags: list = Field(default_factory=list)

user = User()
print(user.tags) # 输出: []

# 生成随机数
class Dice(BaseModel):
value: int = Field(default_factory=lambda: randint(1, 6))

dice = Dice()
print(dice.value) # 输出: 1到6之间的随机数
-----------------------------------------------------------------------------------------
# alias参数
from pydantic import BaseModel, Field

class User(BaseModel):
user_name: str = Field(alias="name")

user = User(name="张伟")
print(user.user_name) # 输出: 张伟
print(user.model_dump(by_alias=True)) # 输出: {'name': '张伟'}
-----------------------------------------------------------------------------------------
# title参数
from pydantic import BaseModel, Field

class User(BaseModel):
name: str = Field(title="用户全名")

print(User.schema()) # 模式中包含 'title': '用户全名'
-----------------------------------------------------------------------------------------
# description参数
from pydantic import BaseModel, Field

class User(BaseModel):
name: str = Field(description="用户的全名")

print(User.schema()) # 模式中包含 'description': '用户的全名'
-----------------------------------------------------------------------------------------
# const参数
from pydantic import BaseModel, Field

class User(BaseModel):
role: str = Field(default="访客", const=True)

user = User(role="访客") # 有效
# user = User(role="管理员") # 抛出ValidationError
-----------------------------------------------------------------------------------------
# gt、ge、lt、le
from pydantic import BaseModel, Field

class Product(BaseModel):
price: float = Field(gt=0) # gt参数

product = Product(price=10.5) # 有效
# product = Product(price=-1) # 抛出ValidationError

class Product(BaseModel):
price: float = Field(ge=0) # ge参数

product = Product(price=0) # 有效
# product = Product(price=-1) # 抛出ValidationError

class Product(BaseModel):
discount: float = Field(lt=100) # lt参数

product = Product(discount=50) # 有效
# product = Product(discount=150) # 抛出ValidationError

class Product(BaseModel):
quantity: int = Field(le=100) # le参数

product = Product(quantity=100) # 有效
# product = Product(quantity=101) # 抛出ValidationError
-----------------------------------------------------------------------------------------
# multiple_of参数,确保数值是指定值的倍数
from pydantic import BaseModel, Field
class Product(BaseModel):
weight: float = Field(multiple_of=0.5)

product = Product(weight=2.5) # 有效
# product = Product(weight=2.3) # 抛出ValidationError
-----------------------------------------------------------------------------------------
# min_length、max_length参数
from pydantic import BaseModel, Field

class User(BaseModel):
password: str = Field(min_length=8) # min_length参数

user = User(password="这是长度足够的密码") # 有效,对于汉字用len()衡量字符长度
# user = User(password="短") # 抛出ValidationError


class User(BaseModel):
username: str = Field(max_length=10) # max_length参数

user = User(username="张伟") # 有效
# user = User(username="一个非常长的用户名超过限制") # 抛出ValidationError
-----------------------------------------------------------------------------------------
# regex参数
from pydantic import BaseModel, Field
class User(BaseModel):
email: str = Field(regex=r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
user = User(email="test@example.com") # 有效
# user = User(email="无效-邮箱") # 抛出ValidationError
-----------------------------------------------------------------------------------------
# exclude参数,序列化时排除字段
from pydantic import BaseModel, Field
class User(BaseModel):
name: str = Field(exclude=True)
age: int

user = User(name="张伟", age=30)
print(user.model_dump()) # 输出: {'age': 30}
-----------------------------------------------------------------------------------------
# repr参数,控制字段是否包含在模型的字符串表示中。
from pydantic import BaseModel, Field
class User(BaseModel):
password: str = Field(repr=False)
name: str

user = User(password="秘密", name="张伟")
print(user) # 输出: User(name='张伟')
-----------------------------------------------------------------------------------------
# validate_default 参数
from pydantic import BaseModel, Field, field_validator
class User(BaseModel):
age: int = Field(default=0, validate_default=True)

@field_validator("age")
def check_age(cls, v):
if v <= 0:
raise ValueError("年龄必须是正数")
return v

user = User(age=10) # 有效
user = User() # 采用默认值age=0,无法通过验证,报错
-----------------------------------------------------------------------------------------
# frozen参数
class User(BaseModel):
id: str = Field(frozen=True)

user = User(id="123")
# user.id = "456" # 抛出ValidationError
-----------------------------------------------------------------------------------------
# allow_inf_nan参数,允许或禁止数字字段使用inf、-inf和nan。
from pydantic import BaseModel, Field
class Data(BaseModel):
value: float = Field(allow_inf_nan=True)

data = Data(value=float("inf")) # 有效
-----------------------------------------------------------------------------------------
# example参数,为字段提供示例值,用于模式生成
from pydantic import BaseModel, Field
class User(BaseModel):
name: str = Field(example="张伟")

print(User.schema()) # 模式中包含 'example': '张伟'

@property 【Pydantic vs Normal]

Pydantic 中的 @property 和 Python 普通类中的 @property 在核心功能上是完全相同的,都是用于创建计算属性。但是,在 Pydantic 模型中使用 @property 时,它会与 Pydantic 自身的数据验证和模型处理机制协同工作,从而产生一些细微但重要的差异。.

普通类中的@property

Python 普通类中,**@property 装饰器用于将一个方法变成一个可以像属性一样访问的特殊方法**。主要目的是:

  • 封装: 隐藏内部实现细节,对外提供统一的属性访问接口。
  • 计算: 属性的值不是直接存储的,而是通过方法动态计算出来的。
  • 控制访问: 可以结合 @setter, @deleter 等实现更复杂的读写控制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import datetime

class Person:
def __init__(self, first_name: str, last_name: str, birth_year: int):
self.first_name = first_name
self.last_name = last_name
self.birth_year = birth_year

@property
def full_name(self) -> str:
"""计算属性:全名"""
return f"{self.first_name} {self.last_name}"

@property
def age(self) -> int:
"""计算属性:年龄"""
current_year = datetime.date.today().year
return current_year - self.birth_year

# 创建实例
p = Person(first_name="张", last_name="三", birth_year=1990)

print(f"姓名: {p.first_name}")
print(f"全名 (通过property): {p.full_name}") # 像访问属性一样调用
print(f"年龄 (通过property): {p.age}") # 像访问属性一样调用

# 直接修改原始属性
p.first_name = "李"
print(f"修改后全名: {p.full_name}") # full_name会随之变化

Pydantic 类中的 @property

Pydantic 模型中,同样可以使用 @property 来定义计算属性。它的语法和基本行为与普通类中的 property 相同。但由于 Pydantic 模型本身带有强大的数据验证和解析能力,Pydantic 中的 @property 也会受益于这些特性,它所依赖的所有输入数据都已通过 Pydantic 强大的验证机制进行了清洗和确认,也就是说如果要用到类属性进行计算,pydantic的@property能保证需要使用的所有的self.attribute都是合法数据类型的,确保 attribute 字段在被赋值给 self.attribute 之前,就已经通过了 Pydantic 的验证,使得计算属性更加健壮,计算的基础数据是有效的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from pydantic import BaseModel, Field
import datetime

class PydanticPerson(BaseModel):
first_name: str
last_name: str
# birth_year 字段自身会受到 Pydantic 的验证
birth_year: int = Field(gt=1900, lt=datetime.date.today().year) # 大于1900年,小于今年

@property
def full_name(self) -> str:
"""Pydantic 模型中的计算属性:全名"""
return f"{self.first_name} {self.last_name}"

@property
def age(self) -> int:
"""Pydantic 模型中的计算属性:年龄"""
current_year = datetime.date.today().year
return current_year - self.birth_year

# 创建实例 (Pydantic 会先验证输入的 first_name, last_name, birth_year)
try:
pp = PydanticPerson(first_name="王", last_name="五", birth_year=1995)
print(f"Pydantic 姓名: {pp.first_name}")
print(f"Pydantic 全名: {pp.full_name}")
print(f"Pydantic 年龄: {pp.age}")
except Exception as e:
print(f"Pydantic 实例化错误: {e}")

print("\n--- Pydantic 验证的优势 ---")
# 尝试创建不合法的 PydanticPerson (birth_year 不合法)
try:
PydanticPerson(first_name="非法", last_name="用户", birth_year=1800) # birth_year 小于 1900
except Exception as e:
print(f"Pydantic 实例化错误 (出生年份太早): {e}") # 会直接在这里报错,而不是后续age计算时才发现

如果类属性存在错误,那么发现错误的时机也不一样

普通类: 如果基础数据本身有问题(比如 birth_year 是个字符串),那么 @property装饰的def age(self)在计算才抛出 TypeError,错误在运行时才暴露。

Pydantic 类: Pydantic 会在模型实例化阶段就验证 birth_year 是否是有效的整数,在数据进入 property 计算之前就阻止了无效数据的传递

Pydantic支持的数据类型

除了自定义的数据类型外,下面还包括了pydantic支持的、 常见的数据类型。

基本Python类型

1
2
3
4
5
6
7
8
9
10
11
str: 字符串
int: 整数
float: 浮点数
bool: 布尔值
list: 列表
dict: 字典
set: 集合
tuple: 元组
bytes: 字节串
bytearray: 可变字节数组
None: 空值(常与 Optional 结合)

标准库类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
datetime.datetime: 日期时间
datetime.date: 日期
datetime.time: 时间
datetime.timedelta: 时间差
uuid.UUID: UUID
pathlib.Path: 文件路径
decimal.Decimal: 高精度小数
fractions.Fraction: 分数
ipaddress.IPv4Address: IPv4 地址
ipaddress.IPv6Address: IPv6 地址
ipaddress.IPv4Network: IPv4 网络
ipaddress.IPv6Network: IPv6 网络
enum.Enum: 枚举
typing.Any: 任意类型
typing.Union: 联合类型(如 Union[str, int])
typing.Optional: 可选类型(如 Optional[str],即 str | None)
typing.List, typing.Dict, typing.Set, typing.Tuple: 泛型集合
typing.Literal: 字面量类型(如 Literal["a", "b"])

Pydantic 特定类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pydantic.EmailStr: 电子邮件地址(验证格式)
pydantic.AnyUrl: 任意 URL
pydantic.HttpUrl: HTTP/HTTPS URL
pydantic.FileUrl: 文件 URL
pydantic.SecretStr: 敏感字符串(隐藏输出)
pydantic.SecretBytes: 敏感字节串
pydantic.Json: JSON 字符串(自动解析为 Python 对象)
pydantic.PositiveInt: 正整数(>0)
pydantic.NonNegativeInt: 非负整数(≥0)
pydantic.NegativeInt: 负整数(<0)
pydantic.NonPositiveInt: 非正整数(≤0)
pydantic.PositiveFloat: 正浮点数(>0)
pydantic.NonNegativeFloat: 非负浮点数(≥0)
pydantic.NegativeFloat: 负浮点数(<0)
pydantic.NonPositiveFloat: 非正浮点数(≤0)
pydantic.PastDate: 过去日期
pydantic.FutureDate: 未来日期
pydantic.PastDatetime: 过去日期时间
pydantic.FutureDatetime: 未来日期时间
pydantic.ByteSize: 字节大小(支持如 "1MB" 的字符串)
pydantic.Color: 颜色(支持 HEX、RGB 等格式)
pydantic.PaymentCardNumber: 信用卡号(验证格式)

常见Pydantic用法

定义简单模型

基础模型定义是 Pydantic 中最常用的功能。通过继承 BaseModel 类,可以定义模型结构和类型约束。每个字段都可以使用 Python 的类型注解来指定其类型,这些类型会在数据验证时被强制执行。

一般习惯于用构造函数传参的方式来构建Pydantic类,但有时候数据可能已经存在字典里了,这时候可以利用Python的关键字参数解包特效来实例化,这也是一种有趣的写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from pydantic import BaseModel

# 定义一个 User 模型
class User(BaseModel):
name: str
age: int
is_active: bool = True # 可以设置默认值

# 创建 User 实例
user1 = User(name="Alice", age=30)
print(user1)
print(f"Name: {user1.name}, Age: {user1.age}, Active: {user1.is_active}")

# 尝试创建不符合类型的数据 (age是字符串)
try:
user2 = User(name="Bob", age="twenty")
except Exception as e:
print(f"\nError creating user2: {e}")

# 尝试创建缺少必要字段的数据
try:
user3 = User(name="Charlie") # age 是必需字段,未提供
except Exception as e:
print(f"\nError creating user3: {e}")


# 解包字典方式实例化对象
user_data = {
"name": "David",
"age": 40,
"addresses": [
{"street": "123 Main St", "city": "Anytown", "zip_code": "12345"},
{"street": "456 Oak Ave", "city": "Otherville", "zip_code": "54321"}
]
}

user_with_addresses = UserWithAddresses(**user_data)

嵌套模型与列表

在实际应用中,数据结构往往是嵌套的。Pydantic 支持模型嵌套,使得可以构建复杂的数据结构,同时保持数据验证的严谨性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from pydantic import BaseModel, Field

# 定义地址模型
class Address(BaseModel):
street: str
city: str
zip_code: str = Field(regex=r"^\d{5}$") # 使用Field定义正则校验邮编

# 定义用户模型,包含一个地址列表,嵌套了
class UserWithAddresses(BaseModel):
name: str
age: int
addresses: list[Address] # 用户可以有多个地址

# 创建实例
user_data = {
"name": "David",
"age": 40,
"addresses": [
{"street": "123 Main St", "city": "Anytown", "zip_code": "12345"},
{"street": "456 Oak Ave", "city": "Otherville", "zip_code": "54321"}
]
}

# 解包字典实例化对象
user_with_addresses = UserWithAddresses(**user_data)
print(user_with_addresses)
print(f"\nUser: {user_with_addresses.name}")
for i, addr in enumerate(user_with_addresses.addresses):
print(f" Address {i+1}: {addr.street}, {addr.city}, {addr.zip_code}")

# 输出结果
name='David' age=40 addresses=[Address(street='123 Main St', city='Anytown', zip_code='12345'), Address(street='456 Oak Ave', city='Otherville', zip_code='54321')]

User: David
Address 1: 123 Main St, Anytown, 12345
Address 2: 456 Oak Ave, Otherville, 54321
-----------------------------------------------------------------------------------------
# 尝试创建不符合邮编规则的数据
try:
invalid_user_data = {
"name": "Eve",
"age": 25,
"addresses": [
{"street": "789 Pine Ln", "city": "Nowhere", "zip_code": "abcde"} # 错误的邮编格式
]
}
UserWithAddresses(**invalid_user_data)
except Exception as e:
print(f"\nError with invalid zip code: {e}") # error

数据转换、别名

数据转换与别名

Pydantic 在验证过程中会自动尝试进行类型转换。还可以使用 Fieldalias 参数来处理输入数据中与模型字段名不一致的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
from pydantic import BaseModel, Field, field_validator
from datetime import date, timedelta

class UserProfile(BaseModel):
first_name: str
last_name: str
birth_date: date
email: str

@property
def full_name(self) -> str:
"""计算属性:用户的全名"""
return f"{self.first_name} {self.last_name}"

@property
def age(self) -> int:
"""计算属性:用户的年龄"""
today = date.today()
return today.year - self.birth_date.year - ((today.month, today.day) < (self.birth_date.month, self.birth_date.day))

@field_validator('email')
@classmethod
def validate_email_domain(cls, v: str) -> str:
"""字段验证器:确保邮箱是有效的 example.com 域名"""
if "@" not in v or not v.endswith(".com"):
raise ValueError("Email must be a valid .com address")
return v

@field_validator('birth_date')
@classmethod
def validate_birth_date(cls, v: date) -> date:
"""字段验证器:确保出生日期不晚于今天"""
if v > date.today():
raise ValueError("Birth date cannot be in the future")
return v

# 创建有效实例
user_profile = UserProfile(
first_name="Jane",
last_name="Doe",
birth_date=date(1990, 5, 10),
email="jane.doe@example.com"
)
print(user_profile)
print(f"\nFull Name: {user_profile.full_name}")
print(f"Age: {user_profile.age}")

# 尝试创建无效邮箱的实例
try:
UserProfile(
first_name="John",
last_name="Smith",
birth_date=date(1985, 1, 1),
email="john.smith@invalid" # 邮箱格式不正确
)
except Exception as e:
print(f"\nError with invalid email: {e}")

# 尝试创建未来出生日期的实例
try:
UserProfile(
first_name="Future",
last_name="Kid",
birth_date=date.today() + timedelta(days=1), # 明天出生
email="future.kid@example.com"
)
except Exception as e:
print(f"\nError with future birth date: {e}")

@field_validator@model_validator

在Pydantic中,@model_validator@field_validator本身已经隐式处理了类方法的绑定,因此不需要显式添加@classmethod装饰器,这是Pydantic的内部实现决定的。尤其是@field_validator@model_validator装饰器会自动将方法视为类方法(mode=’before’或’wrap’),@model_validator(mode='after)'则是装饰为实例方法,并正确处理cls或self,mode='before'接收cls和输入数据,mode='after'操作实例self,无需显式添加@classmethod,尤其是在在@model_validator(mode='after')中,方法更像实例方法(使用self),加@classmethod可能导致逻辑错误或类型检查警告,因为cls和self语义不符。

@field_validator

@field_validator (字段验证器): 用于验证单个字段的值。它只接收被验证字段的值作为输入,并返回处理后的值,具有如下特点:

  • 装饰器参数: 传入一个或多个字段名(字符串)。
  • 方法类型: 必须是 @classmethod原因与 Pydantic 的设计和实现有关,Pydantic 的验证器是与模型类绑定的,验证逻辑需要访问类本身(cls)的上下文。类方法允许 Pydantic 传递模型类(cls)作为第一个参数,便于在验证逻辑中访问类级信息,同时确保验证器在类的命名空间内一致运行,方便 Pydantic 在验证过程中管理字段和模型的关系
  • 输入: 接收当前被验证字段的原始值。
  • 输出: 必须返回处理后的值(如果没有修改,要返回原值)。如果验证失败,抛出 ValueError
  • 执行时机: 在所有 Field 参数验证之后,但在模型实例化之前执行。

使用 @field_validator 装饰的字段,验证函数无需主动调用,只需加上装饰器并指定验证的字段,Pydantic 会在模型实例化或数据验证时自动调用这些方法,验证函数的返回值会被当做属性的最终值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
from pydantic import BaseModel, Field, ValidationError, field_validator
from typing import Optional

class UserRegistration(BaseModel):
username: str = Field(min_length=3, max_length=20)
email: str
password: str = Field(min_length=8)
age: int = Field(gt=0, le=120)
phone_number: Optional[str] = None # 手机号是可选的

@field_validator('username')
@classmethod
def username_must_not_contain_spaces(cls, v: str) -> str:
"""
验证 username 不能包含空格。
v: 当前 username 字段的值。
"""
if ' ' in v:
raise ValueError('用户名不能包含空格')
return v.lower() # 也可以在这里进行数据转换,例如转为小写

@field_validator('email')
@classmethod
def email_must_be_valid_domain(cls, v: str) -> str:
"""
验证 email 必须包含 '@' 且域名部分包含 '.'。
v: 当前 email 字段的值。
"""
if '@' not in v or '.' not in v.split('@')[-1]:
raise ValueError('邮箱格式不正确')
return v

@field_validator('phone_number')
@classmethod
def phone_number_must_be_digits(cls, v: Optional[str]) -> Optional[str]:
"""
如果提供了手机号,验证它必须是纯数字。
v: 当前 phone_number 字段的值。
"""
if v is not None and not v.isdigit():
raise ValueError('手机号必须是纯数字')
return v

# --- 测试 `@field_validator` ---

print("--- 测试 @field_validator ---")

# 1. 成功案例
try:
user1 = UserRegistration(
username="john_doe",
email="john.doe@example.com",
password="securepassword123",
age=30
)
print(f"成功创建用户1: {user1.model_dump_json()}")
print(f"用户名已转为小写: {user1.username}")
except ValidationError as e:
print(f"创建用户1失败: {e}")

# 2. 失败案例:用户名包含空格
try:
UserRegistration(
username="jane doe", # 包含空格
email="jane.doe@example.com",
password="securepassword123",
age=25
)
except ValidationError as e:
print(f"\n创建用户失败 (用户名包含空格): {e}")

# 3. 失败案例:邮箱格式不正确
try:
UserRegistration(
username="mike_t",
email="mike@invalid", # 缺少 '.'
password="securepassword123",
age=40
)
except ValidationError as e:
print(f"\n创建用户失败 (邮箱格式不正确): {e}")

# 4. 失败案例:手机号不是纯数字
try:
UserRegistration(
username="test_user",
email="test@example.com",
password="testpassword",
age=20,
phone_number="123-456" # 包含 '-'
)
except ValidationError as e:
print(f"\n创建用户失败 (手机号不是纯数字): {e}")

@model_validator

@model_validator (模型验证器): 用于验证整个模型的数据(即多个字段之间的关系)。它接收整个模型的字段值(作为字典或实例),并返回处理后的数据。具有如下特点:

  • 装饰器参数: 必须指定 mode='after' (最常用) 或 mode='before'

    • mode='after':在所有字段都通过各自的 Field@field_validator 验证后执行。此时,操作的是已经验证并转换过的模型数据。

    • mode='before':在任何字段验证之前执行。此时,操作的是原始的输入数据(通常是字典)。

  • 输入:

    • mode='after':接收一个 self 对象(看做实例方法),可以通过 self.field_name 访问已验证的字段值。

    • mode='before':接收一个 data 字典,包含所有原始输入字段及其值。

  • 输出:

  • mode='after':必须返回 self (即修改后的模型实例)。

  • mode='before':必须返回 data (即修改后的字典)。

  • 执行时机: 根据 mode 参数在验证流程的不同阶段执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
from pydantic import BaseModel, ValidationError, model_validator
from datetime import date, timedelta

class Event(BaseModel):
start_date: date
end_date: date
# 假设事件名称和描述是可选的
event_name: str
description: str = "无描述"

@model_validator(mode='after')
def check_dates_order(self) -> 'Event':
"""
模型验证器 (mode='after'): 确保结束日期不早于开始日期。
self: 此时是一个 Event 模型的实例,其 start_date 和 end_date 已经通过了各自的字段验证。
"""
if self.end_date < self.start_date:
raise ValueError('结束日期不能早于开始日期')
# 也可以在这里根据其他字段进行逻辑调整
if not self.event_name and not self.description:
raise ValueError('事件必须有名称或描述')
return self

# --- 测试 `@model_validator(mode='after')` ---

print("\n--- 测试 @model_validator(mode='after') ---")

# 1. 成功案例
try:
event1 = Event(
start_date=date(2025, 1, 10),
end_date=date(2025, 1, 15),
event_name="新年派对"
)
print(f"成功创建事件1: {event1.model_dump_json()}")
except ValidationError as e:
print(f"创建事件1失败: {e}")

# 2. 失败案例:结束日期早于开始日期
try:
Event(
start_date=date(2025, 2, 1),
end_date=date(2025, 1, 28), # 结束日期早于开始日期
event_name="错误日期事件"
)
except ValidationError as e:
print(f"\n创建事件失败 (结束日期早于开始日期): {e}")

# 3. 失败案例:既无名称也无描述
try:
Event(
start_date=date(2025, 3, 1),
end_date=date(2025, 3, 5),
event_name="", # 空名称
description="" # 空描述
)
except ValidationError as e:
print(f"\n创建事件失败 (既无名称也无描述): {e}")

# --- 示例:@model_validator(mode='before') ---
# 假设想在验证前,如果提供了 'full_address' 字段,就将其拆分成 'street' 和 'city'
print("\n--- 示例:@model_validator(mode='before') ---")

class AddressInfo(BaseModel):
street: str
city: str
zip_code: str

@model_validator(mode='before')
@classmethod
def parse_full_address(cls, data: dict) -> dict:
"""
模型验证器 (mode='before'): 如果有 'full_address' 字段,就解析它。
data: 原始输入字典。
"""
if isinstance(data, dict) and 'full_address' in data:
full_address_parts = data['full_address'].split(',')
if len(full_address_parts) == 2:
data['street'] = full_address_parts[0].strip()
data['city'] = full_address_parts[1].strip()
else:
raise ValueError('full_address 格式不正确,应为 "街道, 城市"')
del data['full_address'] # 删除原始字段,避免冲突
return data

# 1. 成功案例:通过 full_address 传入
try:
address1 = AddressInfo(
full_address="Main Street, New York",
zip_code="10001"
)
print(f"成功创建地址1 (通过 full_address): {address1.model_dump_json()}")
except ValidationError as e:
print(f"创建地址1失败: {e}")

# 2. 失败案例:full_address 格式不正确
try:
AddressInfo(
full_address="Invalid Address Format",
zip_code="99999"
)
except ValidationError as e:
print(f"\n创建地址失败 (full_address 格式不正确): {e}")

# 3. 成功案例:直接传入 street 和 city
try:
address2 = AddressInfo(
street="Park Avenue",
city="Los Angeles",
zip_code="90001"
)
print(f"成功创建地址2 (直接传入 street 和 city): {address2.model_dump_json()}")
except ValidationError as e:
print(f"\n创建地址2失败: {e}")

Pydantic 的配置(Config)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pydantic import BaseModel, Field, ConfigDict

class StrictModel(BaseModel):
model_config = ConfigDict(extra='forbid', frozen=True) # 禁止额外字段,使实例不可变

name: str
age: int

# 创建有效实例
strict_user = StrictModel(name="Mark", age=28)
print(strict_user)

# 尝试添加额外字段 (会报错)
try:
StrictModel(name="Mark", age=28, city="New York")
except Exception as e:
print(f"\nError with extra field: {e}")

# 尝试修改实例 (如果 frozen=True,会报错)
try:
# 如果 frozen=True,这将抛出 TypeError: "StrictModel" is immutable and does not support item assignment
strict_user.age = 29
except Exception as e:
print(f"\nError modifying frozen instance: {e}")