Pydantic 入门

好久没学过 Python 基础的玩意儿了,最近要用 FastAPI 写点东西,然而 FastAPI 大量使用 Pydantic 用于数据定义和校验,而且之前学的 LangChain 也是大量依赖 Pydantic。这让学习 Pydantic 有实际意义。

所以,就去学。

下面基本把可能会用到的以及不大可能用到的概念都过了一遍,基本已经能够服务于实践并建立对 Pydantic 整体的感知了。给我热情都写没了。


Pydantic,其实就是一个类型定义、校验、序列化、反序列化库,如果要论相似品的话,就是 Typescript 的 zod 加上 Java 的 Jackson。区别在于,Pydantic 通过继承 BaseModel 实现功能,而 zod 更纯,而 Java 使用 POJO 和注解。

Pydantic 有如下值得一提的功能:

  1. 支持序列化、反序列化到 JSON,嵌套地
  2. Strict 和 Lax 模式,前者严格,不进行类型转换,后者宽松,像 Jackson 的反序列化那样宽松
  3. 支持 Dataclass 和 TypedDict
  4. 支持自定义校验器和序列化器

hmmm,这个或许使用 learn x in y minutes 的形式更容易写和学一些。

一个整蛊的地方是,pydantic 把反序列化叫成 validation。

Hello, World

下面是 Pydantic 的 HelloWorld——展示了 schema 中各种字段的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from typing import Literal, Annotated
from pydantic import BaseModel

from annotated_types import Gt
class Fruit(BaseModel):
name: str
color: Literal['red', 'green']
weight: Annotated[float, Gt(0)]
bazam: dict[str, list[tuple[int, bool, float]]]

print(Fruit(
name='apple', # name 不能为其它类型
color='red', # color 必须在 red 和 green 中
weight='100', # weight 可以是字符串形式的数字,但必须大于 0
bazam={'foobar': [(1, True, 0.1)]}, # 嵌套校验
))

注意到它某些地方莫名严格——name 是 str,传 int 不行,而某些地方又莫名宽松,weight 是 float,传 str 可以,这应该是故意设计为如此的,而其行为和我熟悉的 jackson 不合。实际上,这里有个 转换表,描述了所有类型在默认模式(lax)以及 strict 模式下可以的入参。

但我怀疑,JSON 反序列化时可能不走同样的机制(因为这个表里入参都不是 JSON 类型)。

注意——只有初次创建实例时才会进行校验,在其后你可以自由地把值设置地违反约束。

这里也提一下——Pydantic 支持所谓的RootModel——它表示一个数据类型本身是一个 list 或者 str 这样的单数据类型,而并非是一个对象,要比较的话,就是 Jackson 的 JsonValue 注解。RootModel 通常用于生成文档等,它看上去几乎是可以被 TypeAlias(pydantic 提供的我是说,它能提供文档的)代替的……只不过 TypeAlias 没有直接提供校验功能,需要让 TypeAdapater 协助:

1
2
3
4
5
6
7
from pydantic import RootModel

class SomeRequestBody(RootModel[list[str]]):
pass

v = SomeRequestBody.model_validate_json(""" ["1", "2"] """)
print(v.root) # 需要通过 root 字段去访问

序列化到 dict 或 JSON

关于序列化,pydantic 支持三种方式——

  1. 序列化到 dict,字段包含 python 对象,如 datetime 等
  2. 序列化到 dict,所有字段类型和 json 兼容
  3. 序列化到 JSON

前两者使用model_dump方法,后者使用model_dump_json方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from datetime import date, datetime
from typing import Annotated
from pydantic import BaseModel

from annotated_types import Gt

class Idol(BaseModel):
name: str
age: Annotated[int, Gt(0)]
join_date: date


idol = Idol(name='Chihaya', age=16, join_date=datetime.strptime('2004-01-01', '%Y-%m-%d'))

print(idol.model_dump()) # 还有一堆配置项,如排除 None,排除默认值等
print(idol.model_dump(mode='json'))
print(idol.model_dump_json()) # 能配置缩进等,就像 json 库一样

此外,也能使用dict函数去直接把 model 转成 dict,但这样操作只能转换顶层为 dict,这大多数时候恐怕都不是我们想要的结果。

此外,model 能通过model_json_schema方法去生成 JSON Schema(似乎是兼容 OpenAPI 格式的,即 Swagger 那个),不过这里不表,因为我不会显式地用它。

从 dict 或 JSON 反序列化

从 dict 反序列化,有两种方式,使用构造函数或者model_validate方法,这两种方式就使用上来说好像 tmd 没有任何差别,我还以为至少在别名等地方有差别呢……

1
2
3
4
5
6
7
8
9
10
11
from pydantic import BaseModel

class Clazz(BaseModel):
name: str

class Idol(BaseModel):
name: str
clazz: Clazz

print(Idol(name='chihaya',clazz={'name': '765'}))
print(Idol.model_validate(dict(name='chihaya',clazz={'name': '765'})))

还有一个model_validate_strings……它相较于model_validate似乎在类型转换上更加智能,允许所有字段全部是 string

然后是model_validate_json,从 JSON 字符串或 bytes 到 model,这个不言自明。

此外,还有一个model_construct方法直接构造 model,不进行校验,这个很坏。

Schema 的花式定义法

自定义类型和约束

Pydantic 既支持 Python 内置类型,也支持自定义类型的校验,同时它也提供了一些 Helper 类型,定制序列化反序列化的过程,如 StrictXXX,不表。

Pydantic 支持常见的标准库类型,大部分地方都是符合直觉的,所以标准库类型不表,只需要知道,datetime 支持我们习惯的 YYYY-MM-DD HH:MM:SS的格式(非严格时支持YYYY-MM-DD),date 则支持YYYY-MM-DD,很方便,就这点来说,沿用 Jackson 的经验已足够。

重点是,Pydantic 允许添加自定义规则的类型,比如,我们想要大于 0 的整型(有没有想到 Dependent Type?哈哈),我们就这么写。注意下面的 TypeAdapter,它相当于是利用类型的元信息构造的一个序列化、反序列化器

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

from typing import Annotated
from pydantic import BaseModel, Field, TypeAdapter, ValidationError

# Annotated 只是增加元数据,就外观上来说,它仍旧是 int
# 这个类型有两种应用方式——要么作为 BaseModel 的字段,要么使用 TypeAdapter
PositiveInt = Annotated[int, Field(gt=0)]

class TestClz(BaseModel):
data: PositiveInt

try:
TestClz(data=-1)
except ValidationError as e:
print('成功地失败了', e)

ta = TypeAdapter(PositiveInt)

try:
v = ta.validate_python(-1)
except ValidationError as e:
print('成功地失败了', e)

# 最酷的地方在于,可以像 java 的嵌套的注解一样玩儿:
NotEmptyPositiveList = Annotated[list[Annotated[int, Field(gt=0)]], Field(min_length=1)]

ta= TypeAdapter(NotEmptyPositiveList)

try:
ta.validate_python(None)
except:
print('successfully failed')
try:
ta.validate_python([])
except:
print('successfully failed')
try:
ta.validate_python([-1])
except:
print('successfully failed')

ta.validate_python([1,2])
print('success')

后面和 dataclass,TypedDict,NamedTuple 协同工作,同样要使用 TypeAdapter

不过这里算是跑题了,现在问题是,有哪些像 gt 这样的规则和约束,如何自定义约束?大部分情况下,使用上面示例中的 Field 函数,它类似标准库中 dataclass 的field函数,但它放在 Annotated 注解中,这个注解用于添加元信息,Pydantic 大量使用这个模式。

Field 包含这些重要配置可能会常用:

  1. title, description:JSON Schema 中的标题,描述等,这个在 LangChain 中要求结构化输出时有意义
  2. default, default_factory: 字面意思,但 pydantic 似乎不会有共享可变对象的问题
  3. alias:字段别名,可以分别序列化、反序列化时使用的别名
  4. gt, ge, lt, le, multiple_of, allow_inf_nan:数字约束;疑惑为什么没有 eq?直接用 Literal
  5. min_length, max_length, pattern:字符串约束,但 min_length 和 max_length 都支持所有可迭代类型
  6. strict:严格校验模式
  7. frozen:不可变

Field 以外的元信息

Field 这个究竟叫什么好像没找到专门的术语,但除了 Field 以外,Pydantic 还支持其它的元信息,它们也是通过 Annotated 去使用的。以及,FastAPI 等框架也会依赖 Annotated 提供功能。

这里主要说的是内置库中提供的更多约束,实际上这些约束均在annotated_type包下,一些可能会用到且 Field 没有覆盖到的约束包括:

  1. Interval:相当于是 gt,lt,ge,le 的组合
  2. Len:相当于是 min_length 和 max_length 的组合
  3. Timezone:限制时区
  4. Unit:数字的单位,如克,米等,这个好像没屁用
  5. Predicate:接受 Callable,自定义断言!自定义断言在类型转换后才开始操作,所以可以安心!
  6. Not:这是一个 dataclass,接受一个函数,行为为将函数的返回值取反
  7. LowerCase,UpperCase,IsDigit:一堆使用 Annotated 和 Predicate 预定义的类型(注意是类型!它作为元信息是没效果的!)

下面随意展示一下它的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pydantic import BaseModel, Field
from typing import Annotated
from annotated_types import LowerCase, Predicate, IsDigit

class Idol(BaseModel):
name: Annotated[LowerCase, Field(pattern=r'\w+ \w+')] # bound=str
clazz: IsDigit # bound=str
age: Annotated[int, Predicate(lambda age: age < 18)] # Idol 永远年轻!

print(Idol(name='amami chihaya', clazz='765', age='16')) # 注意 Predicate 在类型转换后才执行
# Idol(name='abc', clazz='123') # fail with pattern
# Idol(name='ABC DEF', clazz='123') # fail with str.islower
# Idol(name='abc def', clazz='123', age = '19') # fail with predicate

计算字段

这个只用来序列化,相当于是在 jackson 中给 getter 加 JsonProperty。注意顺序!!!computed_field 必须接受 property,这意味着它必须放在 property 外头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

from typing import Annotated
from annotated_types import Gt
from pydantic import BaseModel, Field, TypeAdapter, ValidationError, computed_field

class Idol(BaseModel):
first_name: str
last_name: str

@computed_field # 注意顺序!!!!!!!!
@property
def full_name(self) -> str:
return self.first_name + ' ' + self.last_name


print(Idol(first_name='Chihaya', last_name='Amami').model_dump_json())

模式匹配(Discriminated Unions)

首先,Pydantic 支持 Union 类型,即str | int | NonePydantic 会选择最匹配的类型

就像 zod 一样,Pydantic 支持 Discriminated Union(就是 ts 的模式匹配那一套,根据特定字段的值去具体确定一个对象的类型),但 Pydantic 的相对更为复杂一些——Python 是强类型语言,在 js 里一切皆 object,但 Python 中要区分成不同的类,而且这些类互相之间是没有继承关系的。

但在此之前,我们先验证一下 Python 本身是否支持 ts 的模式匹配,使用下面的代码,检查 IDE 是否正确识别下面的 v 究竟是 Nothing 还是 Just。

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
@dataclass
class Nothing[T]:
type: Literal['NOTHING'] = field(default='NOTHING')

@dataclass
class Just[T]:
value: T
type: Literal['JUST'] = field(default='JUST')

type Maybe[T] = Nothing[T] | Just[T]

def some_maybe(i: int) -> Maybe[int]:
if i % 2 == 0:
return Just(i)
return Nothing()

v = some_maybe(111)

if v.type == 'JUST':
v.value # int
else:
v.type # NOTHING

if v.type == 'NOTHING':
v.value # Any
else:
v.value # int

但是,我为什么要学 ts?Python 的类本身就有类型信息,为什么要一个 type 字段?下面的实现更现代,更 Python:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@dataclass
class Nothing[T]: pass

@dataclass
class Just[T]:
value: T

type Maybe[T] = Nothing[T] | Just[T]

v = cast(Maybe[int], Just(100))

match v:
case Just(x):
print(f'just {x}')
case Nothing():
print('nothing')

直接展示代码,这代码会给人一点惊吓。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

from typing import Annotated, Literal, TypeAliasType, cast
from annotated_types import Gt
from pydantic import BaseModel, Field, TypeAdapter, ValidationError, computed_field

class Nothing[T](BaseModel):
type: Annotated[Literal['NOTHING'], Field(default='NOTHING')]

class Just[T](BaseModel):
value: T
type: Annotated[Literal['JUST'], Field(default='JUST')]

type Maybe[T] = Annotated[Nothing[T] | Just[T], Field(discriminator='type')]

class WTF_MAN[T](BaseModel):
maybe: Maybe[T]

# 我操这是什么玩法???
print(WTF_MAN[int].model_validate({'maybe': {'type': 'NOTHING'}}))
print(WTF_MAN[int].model_validate({'maybe': {'type': 'JUST', 'value': 100}}))

函数参数校验

一个经典的问题是校验入参,Pydantic 提供了一个装饰器 validate_call,支持对函数的参数进行校验。这个装饰器同时支持 async 函数,支持不定参,支持关键字参数,以及支持校验返回值(默认不开启)……总之支持 Python 函数的一切特性,只能说牛逼。

而且这个装饰器还带来一个好处——Pydantic 顺便还把隐式类型转换给做了,这意味着我们可以依赖它,比如直接传给 datetime 字段一个日期字符串,如果不想要这个,可以配置它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

from pydantic import ConfigDict, validate_call

@validate_call
def add(a: int, b: int):
return a + b
print(add(1, '2')) # 3(!!注意 Pydantic 的类型转换!!)

@validate_call(config=ConfigDict(strict=True))
def add(a: int, b: int):
return a + b
try:
print(add(1, '2'))
except:
print('strict!')

遗憾的是,这个装饰器不支持泛型

和 dataclass,TypedDict,NamedTuple 协同工作

首先,Pydantic 提供了一个自己的 dataclasses 模块,它和标准库的 dataclass 兼容,只需要使用 Pydantic 提供的 dataclass 便可以利用上 Pydantic 的校验机制,这在不想使用 BaseModel 而想使用 dataclass 那样的模式又想品尝到校验机制时可以选择。这里不表。

要和 dataclass,TypedDict,NamedTuple 一起工作,即和原始 Python 类型的类型标注协同工作,需要使用 TypeAdapter,这里的 Adapter 的意思,显然就是将 Python 类型标注适配为 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
from typing import TypedDict, NamedTuple
from dataclasses import dataclass
from pydantic import TypeAdapter

class Idol1(TypedDict):
name: str
age: int

class Idol2(NamedTuple):
name: str
age: int

Idol3 = NamedTuple('Idol3', [('name', str), ('age', int)])

@dataclass
class Idol4:
name: str
age: int

data = {'name': 'amami haruka', 'age': '17'}

ta = TypeAdapter(Idol1)
print(ta.validate_python(data))
ta = TypeAdapter(Idol2)
print(ta.validate_python(data))
ta = TypeAdapter(Idol3)
print(ta.validate_python(data))
ta = TypeAdapter(Idol4)
print(ta.validate_python(data))

自定义校验(即反序列化)

就像 Hibernate Validator 和 Jackson 的 Serde,Pydantic 支持配置字段和模型级别的校验器,其中均允许配置前校验、后校验,环绕校验——对原始输入进行校验,对类型转换后的数据进行校验,对转换前后的数据进行校验

校验器是什么呢?可以认为,校验器就是接受输入,然后返回校验后的值的一个 Callable,这个值可以和原值不同(做修改,或者类型转换等)。

同时,这个 Callable 可以抛出异常,Pydantic 支持ValueErrorAssertionErrorPydanticCustomError,Pydantic 会将这些异常包装成 ValidationError。Assertion 看着很香,但注意它在生产环境可能是会被关闭的!

字段校验

有两种方式去自定义校验器——使用 Annotated 模式,或者使用 field_validator 装饰器,显然前者供复用,后者临时用。

最常用的是后校验——后校验在 Pydantic 内置校验之后发生,因此类型安全,容易实现。下面同时展示 Annotated 模式和 Decorator 模式的用法:

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
from typing import Annotated
from pydantic import AfterValidator, field_validator, BaseModel, ValidationError

def is_even(i: int):
assert i % 2 == 0
return i

class SomeModel(BaseModel):
field1: Annotated[int, AfterValidator(is_even)]
field2: int

@field_validator('field2')
@classmethod
def is_even(cls, i: int) -> int:
if i % 2 != 0:
raise ValueError(f'{i} is not an even number')
return i

try:
SomeModel(field1=1, field2=2)
except Exception as e:
print(e)
try:
SomeModel(field1=2, field2=1)
except Exception as e:
print(e)

前面提到,后校验发生在 Pydantic 内部校验之后,后校验的输入是 Pydantic 内部校验的输出,输出是用户看到的值,而前校验,输入是原始输入,输出喂给 Pydantic 的内部校验

前校验器显然是更灵活的——它接受任意的输入并作处理然后再做后续流程。考虑一个经典问题:使用一个字符串传多个半角逗号分割的 ID 集合。下面同时以两种方式写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from typing import Annotated
from pydantic import BeforeValidator, field_validator, BaseModel

def collection_str(ids_str):
if isinstance(ids_str, str):
return ids_str.split(',')
if isinstance(ids_str, list):
return ids_str
return [str(ids_str)]

class SomeModel(BaseModel):
field1: Annotated[list[str], BeforeValidator(collection_str)]
field2: list[str]

@field_validator('field2', mode='before')
@classmethod
def collection_str(cls, i):
return collection_str(i)

print(SomeModel(field1='1,2,3', field2=['123','234']))

注意——前校验器抛出异常不代表校验失败,比如 Union 类型,左边的类型校验失败后 Pydantic 会尝试右边的类型,这意味着前校验器不应当修改可变对象

然后有个所谓的 PlainValidator,这个……它行为是 BeforeValidator,然而终止后续流程,直接把结果返回给用户

最灵活的则是环绕校验,最灵活,它类似 Servlet 的 Filter,直接展示它的定义(注意它只有 Annotated 模式,Decorator 模式不支持它):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pydantic import ValidatorFunctionWrapHandler, ValidationError

def do_nothing(value, handler: ValidatorFunctionWrapHandler):
# 前校验器、Pydantic 内部校验之前
# do something...
try:
result = handler(value) # 前校验器,Pydantic 内部校验,后校验器
except ValidationError:
# 捕获这些校验器产生的异常
# do something...
raise

# Pydantic 内部校验,后校验器之后
# do something...
return result

模型校验

说句题外话,Pydantic 中没有像 Hibernate Validator 那样的校验组机制。

在 Hibernate Validator 中,我只使用过字段校验,然而实际上它也是支持模型校验的。Pydantic 同样支持模型校验,即对整个模型进行校验,因此可以看到字段之间的关系

模型校验支持后校验,前校验,和环绕校验。考虑到模型校验是无法重用的,Pydantic 只提供了 Decorator 模式去实现校验。具体的说,使用model_validator装饰器。

后校验使用实例方法(因此是实例化后执行的),前校验和环绕校验使用类方法。这里只写后校验的示例,因为我已经有点麻了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pydantic import model_validator, BaseModel

class ResetPasswordDto(BaseModel):
username: str
new_password: str
new_password_repeat: str

@model_validator(mode='after') # mode 必须给定
def check_password_match(self):
if self.new_password != self.new_password_repeat:
raise ValueError('Passwords do not match')
return self # 还能换成别人??

ResetPasswordDto(username='abc', new_password='pass', new_password_repeat='pass')

我不研究各个校验器的执行顺序——我又不是去面试!真遇到问题再说!

自定义序列化

配置序列化有四种方式,@field_serializer@model_serializerPlainSerializerWrapSerializer

@field_serializer不能配置 mode,@model_serialzer能配置 wrap 或者 plain,显然@field_serializermodel_serializer配置为 plain 时是不会起效的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pydantic import field_serializer, model_serializer, BaseModel

class Hello(BaseModel):
hello: str

# 如果签名没有返回值类型,需要配置 return_type 指定返回类型
# when_used='json' 表示只有序列化成 json(兼容)模式时才生效
# 要知道,Pydantic 是能把模型序列化成 dict 的
@field_serializer('hello', when_used='json')
def ser_hello(x: str) -> str:
return x.upper()

# 会覆盖掉上面的 field_serializer
# @model_serializer(mode='plain')
# def ser_self(self):
# return {'hello': 'world!'}

h = Hello(hello='world')
print(h.model_dump()) # {'hello': 'world'}
print(h.model_dump(mode='json')) # {'hello': 'WORLD'}
print(h.model_dump_json()) # {"hello":"WORLD"}

两个 Annotated 模式的 Serializer,显然是适用于字段的,使用方式和上面的一致,这里不表了。

关于日期的序列化、反序列化

接下来是一个究极经典的问题——对日期的序列化、反序列化,就像 Jackson 的 JsonFormat 注解。

首先,Pydantic 对 date,datetime,默认的序列化、反序列化逻辑是怎样的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from datetime import datetime, date
from typing import Annotated
from pydantic import BeforeValidator, PlainSerializer, TypeAdapter

date_ta = TypeAdapter(date)
datetime_ta = TypeAdapter(datetime)

now_datetime = datetime.now()

date_ta.validate_python(date(year=2023,month=1,day=1)) # passed
date_ta.validate_python(datetime(year=2023,month=1,day=1)) # passed
date_ta.validate_python(now_datetime.strftime('%Y-%m-%d')) # passed
date_ta.validate_python(now_datetime.strftime('%Y-%m-%d 00:00:00')) # passed

# failed!!!
# date 的所有时间必须全部是 0!
# date_ta.validate_python(now_datetime.strftime('%Y-%m-%d %H:%M:%S'))

datetime_ta.validate_python(date(year=2023,month=1,day=1)) # passed
datetime_ta.validate_python(datetime(year=2023,month=1,day=1)) # passed
datetime_ta.validate_python(now_datetime.strftime('%Y-%m-%d')) # passed
datetime_ta.validate_python(now_datetime.strftime('%Y-%m-%d 00:00:00')) # passed

注意到:

  1. date,datetime 均接受我们最熟悉的YYYY-MM-ddYYYY-MM-dd HH:mm:ss格式
  2. datetime 接受精确到时分秒的表示,但也能接受日期表示(后者在 strict 模式下不合法)
  3. date 接受日期表示,但也能接受精确到时分秒的表示,只是时分秒必须为 0

我想到两种方式:

  1. 使用类似注解的形式——提供一个可配置的元信息去模拟 JsonFormat 注解
  2. 提供预先添加上面的元信息的类型

其实两种都是同样的对不对?这里操作一个:

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
from datetime import datetime, date
from typing import Annotated
from pydantic import BeforeValidator, PlainSerializer, TypeAdapter

def time_formatter(pattern: str):
# 校验 pattenr 是否是合法 pattern
datetime.now().strftime(pattern)

def ser(x: datetime | date) -> str:
return x.strftime(pattern)

def deser(x) -> datetime:
return datetime.strptime(str(x), pattern)

return BeforeValidator(deser), PlainSerializer(ser)

# 这里得做一个解构……似乎也有不用解构的方式,见 annotated_types 包下的 Interval
YYYYMMDD_DATE = Annotated[date, *time_formatter('%Y--%m--%d')]

# 换成字面量就是第一种了
ta = TypeAdapter(YYYYMMDD_DATE)

deser_test = ta.validate_python('2025--12--31')
print(type(deser_test), deser_test) # <class 'datetime.date'> 2025-12-31

ser_test = ta.dump_json(deser_test)
print(ser_test) # b'"2025--12--31"'

YYYYMMDD_HHMMSS_DATETIME = Annotated[datetime, *time_formatter('%Y-%m-%d//%H:%M:%S')]
ta = TypeAdapter(YYYYMMDD_HHMMSS_DATETIME)

d = ta.validate_python('2025-12-31//12:34:56')
print(type(d), d) # <class 'datetime.datetime'> 2025-12-31 12:34:56
print(ta.dump_json(d)) # b'"2025-12-31 12:34:56"'

配置 Model

Pydantic 支持配置 Model 的校验、序列化配置,这些配置能够影响其所有字段的行为(实际上就是处理 Pydantic 关于内部校验的行为)。该配置利用所谓的ConfigDict类。配置似乎是不会递归影响的,只会影响本层。

无论是 BaseModel,Typeadapter,Pydantic 的 dataclass,还是 Python 自己的 dataclass,TypedDict 等,均允许进行配置。

  • 对 BaseModel,直接设置类字段model_config
  • 对 Pydantic 的 dataclass,设置装饰器中的 config 字段。
  • 对 TypeAdapter,通过参数传递。
  • 对 Python 的 dataclass,设置__pydantic_config__字段或者使用with_config装饰器。
  • 对 TypedDict,只能使用with_config装饰器。

ConfigDict 中具体能配置这些信息:

  1. JSON Schema 相关的生成
  2. 字符串的默认处理(全大写,小写,strip,长度等)
  3. 对多余字段的处理,默认是忽略,可以配置为 allow,这时候多余字段会存储到__pydantic_extra__字段下
  4. 是否 frozen,是否 strict……
  5. 关于枚举的配置
  6. JSON 中日期的序列化方式,JSON 字节串的编解码方式

就这样吧。