实验性功能¶
在本节中,您将找到 Pydantic 中新的实验性功能的文档。这些功能可能会更改或移除,我们希望在将其永久纳入 Pydantic 之前收集反馈和建议。
有关实验性功能的更多信息,请参阅我们的版本政策。
反馈¶
我们欢迎对实验性功能的反馈!请在 Pydantic GitHub 仓库上提出一个 issue,分享您的想法、请求或建议。
我们也鼓励您阅读现有的反馈,并将您的想法添加到已有的 issue 中。
管道 API¶
Pydantic v2.8.0 引入了一个实验性的“管道”API,它允许以比现有 API 更类型安全的方式组合解析(验证)、约束和转换。此 API 可能会更改或移除,我们希望在将其永久纳入 Pydantic 之前收集反馈和建议。
通常,管道 API 用于定义在验证期间应用于输入数据的一系列步骤。管道 API 的设计旨在比现有的 Pydantic API 更具类型安全性和可组合性。
管道中的每一步可以是:
- 一个验证步骤,对提供的类型运行 pydantic 验证
- 一个转换步骤,修改数据
- 一个约束步骤,根据某个条件检查数据
- 一个断言步骤,根据某个条件检查数据,如果返回
False
则引发错误
请注意,以下示例为了详尽而牺牲了简洁性:如果您发现自己在类型注解中编写了如此多的转换,您可能需要考虑使用 UserIn
和 UserOut
模型(如下例所示)或类似方式,通过惯用的纯 Python 代码进行转换。这些 API 适用于代码节省显著且增加的复杂性相对较小的情况。
from __future__ import annotations
from datetime import datetime
from typing import Annotated
from pydantic import BaseModel
from pydantic.experimental.pipeline import validate_as
class User(BaseModel):
name: Annotated[str, validate_as(str).str_lower()] # (1)!
age: Annotated[int, validate_as(int).gt(0)] # (2)!
username: Annotated[str, validate_as(str).str_pattern(r'[a-z]+')] # (3)!
password: Annotated[
str,
validate_as(str)
.transform(str.lower)
.predicate(lambda x: x != 'password'), # (4)!
]
favorite_number: Annotated[ # (5)!
int,
(validate_as(int) | validate_as(str).str_strip().validate_as(int)).gt(
0
),
]
friends: Annotated[list[User], validate_as(...).len(0, 100)] # (6)!
bio: Annotated[
datetime,
validate_as(int)
.transform(lambda x: x / 1_000_000)
.validate_as(...), # (8)!
]
- 将字符串转换为小写。
- 约束一个整数大于零。
- 约束一个字符串匹配正则表达式模式。
- 您也可以使用更底层的 transform、constrain 和 predicate 方法。
- 使用
|
或&
操作符来组合步骤(类似逻辑或与逻辑与)。 - 使用
Ellipsis
或...
作为第一个位置参数调用validate_as(...)
意味着validate_as(<field type>)
。使用validate_as(Any)
来接受任何类型。 - 您可以在其他步骤之前或之后调用
validate_as()
来进行预处理或后处理。
从 BeforeValidator
、AfterValidator
和 WrapValidator
映射¶
validate_as
方法是定义 BeforeValidator
、AfterValidator
和 WrapValidator
的一种更类型安全的方式。
from typing import Annotated
from pydantic.experimental.pipeline import transform, validate_as
# BeforeValidator
Annotated[int, validate_as(str).str_strip().validate_as(...)] # (1)!
# AfterValidator
Annotated[int, transform(lambda x: x * 2)] # (2)!
# WrapValidator
Annotated[
int,
validate_as(str)
.str_strip()
.validate_as(...)
.transform(lambda x: x * 2), # (3)!
]
- 在将字符串解析为整数之前去除其空白字符。
- 将一个整数解析后乘以 2。
- 去除字符串的空白字符,将其验证为整数,然后乘以 2。
替代模式¶
根据不同场景,有许多替代模式可供使用。例如,考虑上面提到的 UserIn
和 UserOut
模式:
from __future__ import annotations
from pydantic import BaseModel
class UserIn(BaseModel):
favorite_number: int | str
class UserOut(BaseModel):
favorite_number: int
def my_api(user: UserIn) -> UserOut:
favorite_number = user.favorite_number
if isinstance(favorite_number, str):
favorite_number = int(user.favorite_number.strip())
return UserOut(favorite_number=favorite_number)
assert my_api(UserIn(favorite_number=' 1 ')).favorite_number == 1
这个例子使用了纯粹惯用的 Python 代码,可能比上面的例子更容易理解、进行类型检查等。您选择的方法应真正取决于您的用例。您将需要比较代码的冗长性、性能、向用户返回有意义错误信息的难易程度等来选择正确的模式。但要小心,不要仅仅因为可以就滥用像管道 API 这样的高级模式。
部分验证¶
Pydantic v2.10.0 引入了对“部分验证”的实验性支持。
这允许您验证一个不完整的 JSON 字符串,或者一个代表不完整输入数据的 Python 对象。
部分验证在处理 LLM 的输出时特别有用,因为模型会以流式方式返回结构化响应,您可能希望在接收数据的同时就开始验证数据流(例如,为了向用户展示部分数据)。
警告
部分验证是一项实验性功能,在 Pydantic 的未来版本中可能会发生变化。当前的实现应被视为概念验证,并且存在一些局限性。
在使用 TypeAdapter
的三个验证方法时可以启用部分验证:TypeAdapter.validate_json()
、TypeAdapter.validate_python()
和 TypeAdapter.validate_strings()
。这不仅允许您解析和验证不完整的 JSON,还可以验证通过解析任何格式的不完整数据创建的 Python 对象。
experimental_allow_partial
标志可以传递给这些方法以启用部分验证。它可以取以下值(默认为 False
):
False
或'off'
- 禁用部分验证True
或'on'
- 启用部分验证,但不支持尾随字符串'trailing-strings'
- 启用部分验证并支持尾随字符串
'trailing-strings'
模式
'trailing-strings'
模式允许将部分 JSON 末尾不完整的字符串包含在输出中。例如,如果您正在根据以下模型进行验证:
from typing import TypedDict
class Model(TypedDict):
a: str
b: str
那么以下 JSON 输入将被视为有效,尽管末尾的字符串不完整:
'{"a": "hello", "b": "wor'
并且会被验证为:
{'a': 'hello', 'b': 'wor'}
experiment_allow_partial
的实际应用
from typing import Annotated
from annotated_types import MinLen
from typing_extensions import NotRequired, TypedDict
from pydantic import TypeAdapter
class Foobar(TypedDict): # (1)!
a: int
b: NotRequired[float]
c: NotRequired[Annotated[str, MinLen(5)]]
ta = TypeAdapter(list[Foobar])
v = ta.validate_json('[{"a": 1, "b"', experimental_allow_partial=True) # (2)!
print(v)
#> [{'a': 1}]
v = ta.validate_json(
'[{"a": 1, "b": 1.0, "c": "abcd', experimental_allow_partial=True # (3)!
)
print(v)
#> [{'a': 1, 'b': 1.0}]
v = ta.validate_json(
'[{"b": 1.0, "c": "abcde"', experimental_allow_partial=True # (4)!
)
print(v)
#> []
v = ta.validate_json(
'[{"a": 1, "b": 1.0, "c": "abcde"},{"a": ', experimental_allow_partial=True
)
print(v)
#> [{'a': 1, 'b': 1.0, 'c': 'abcde'}]
v = ta.validate_python([{'a': 1}], experimental_allow_partial=True) # (5)!
print(v)
#> [{'a': 1}]
v = ta.validate_python(
[{'a': 1, 'b': 1.0, 'c': 'abcd'}], experimental_allow_partial=True # (6)!
)
print(v)
#> [{'a': 1, 'b': 1.0}]
v = ta.validate_json(
'[{"a": 1, "b": 1.0, "c": "abcdefg',
experimental_allow_partial='trailing-strings', # (7)!
)
print(v)
#> [{'a': 1, 'b': 1.0, 'c': 'abcdefg'}]
- TypedDict
Foobar
有三个字段,但只有a
是必需的,这意味着即使缺少b
和c
字段,也可以创建Foobar
的有效实例。 - 解析 JSON 时,输入在字符串被截断之前是有效的 JSON。
- 在这种情况下,输入的截断意味着
c
的值(abcd
)作为c
字段的输入是无效的,因此它被省略了。 a
字段是必需的,因此对列表中唯一项的验证失败并被丢弃。- 部分验证也适用于 Python 对象,其语义应与 JSON 相同,当然,您不能有一个真正“不完整”的 Python 对象。
- 与上面相同,但使用 Python 对象,
c
因为不是必需的且验证失败而被丢弃。 trailing-strings
模式允许将部分 JSON 末尾不完整的字符串包含在输出中,在这种情况下,输入在字符串被截断之前是有效的 JSON,所以最后一个字符串被包含在内。
部分验证的工作原理¶
部分验证遵循 Pydantic 的禅道 —— 它不保证输入数据可能是什么样的,但它保证返回您所要求类型的有效实例,或者引发验证错误。
为了做到这一点,experimental_allow_partial
标志启用了两种行为:
1. 部分 JSON 解析¶
Pydantic 使用的 jiter JSON 解析器已经支持解析部分 JSON,experimental_allow_partial
只是通过 allow_partial
参数传递给 jiter。
注意
如果您只想要支持部分 JSON 的纯粹 JSON 解析,您可以直接使用 jiter
Python 库,或者在调用 pydantic_core.from_json
时传递 allow_partial
参数。
2. 忽略输入最后一个元素中的错误¶
由于只能访问部分输入数据,错误通常会发生在输入数据的最后一个元素中。
例如:
- 如果一个字符串有约束
MinLen(5)
,当您只看到部分输入时,验证可能会因为部分字符串缺失而失败(例如{"name": "Sam
而不是{"name": "Samuel"}
) - 如果一个
int
字段有约束Ge(10)
,当您只看到部分输入时,验证可能会因为数字太小而失败(例如1
而不是10
) - 如果一个
TypedDict
字段有3个必需字段,但部分输入只有其中两个字段,验证会因为某些字段缺失而失败 - 等等 —— 还有很多类似的情况
关键是,如果您只看到部分有效的输入数据,验证错误通常会发生在序列的最后一个元素或映射的最后一个值中。
为了避免这些错误破坏部分验证,Pydantic 将忽略输入数据最后一个元素中的所有错误。
from typing import Annotated
from annotated_types import MinLen
from pydantic import BaseModel, TypeAdapter
class MyModel(BaseModel):
a: int
b: Annotated[str, MinLen(5)]
ta = TypeAdapter(list[MyModel])
v = ta.validate_json(
'[{"a": 1, "b": "12345"}, {"a": 1,',
experimental_allow_partial=True,
)
print(v)
#> [MyModel(a=1, b='12345')]
部分验证的局限性¶
仅限 TypeAdapter¶
您只能将 experiment_allow_partial
传递给 TypeAdapter
的方法,其他 Pydantic 入口点(如 BaseModel
)尚不支持。
支持的类型¶
目前只有一部分集合验证器知道如何处理部分验证:
list
set
frozenset
dict
(如dict[X, Y]
)TypedDict
— 只有非必需字段可以缺失,例如通过NotRequired
或total=False
)
虽然您可以在对包含其他集合验证器的类型进行验证时使用 experimental_allow_partial
,但这些类型将被“全有或全无”地验证,部分验证不会在更深层嵌套的类型上起作用。
例如,在上面的例子中,部分验证是有效的,尽管列表中的第二项被完全丢弃了,因为 BaseModel
(尚)不支持部分验证。
但在以下示例中,部分验证将完全不起作用,因为 BaseModel
不支持部分验证,所以它不会将 allow_partial
指令向下传递给 b
中的列表验证器。
from typing import Annotated
from annotated_types import MinLen
from pydantic import BaseModel, TypeAdapter, ValidationError
class MyModel(BaseModel):
a: int = 1
b: list[Annotated[str, MinLen(5)]] = [] # (1)!
ta = TypeAdapter(MyModel)
try:
v = ta.validate_json(
'{"a": 1, "b": ["12345", "12', experimental_allow_partial=True
)
except ValidationError as e:
print(e)
"""
1 validation error for MyModel
b.1
String should have at least 5 characters [type=string_too_short, input_value='12', input_type=str]
"""
b
的列表验证器没有从模型验证器那里接收到allow_partial
指令,所以它不知道要忽略输入最后一个元素中的错误。
一些无效但完整的 JSON 将被接受¶
jiter(Pydantic 使用的 JSON 解析器)的工作方式意味着目前无法区分完整的 JSON(如 {"a": 1, "b": "12"}
)和不完整的 JSON(如 {"a": 1, "b": "12
)。
这意味着在使用 experimental_allow_partial
时,Pydantic 会接受一些无效的 JSON,例如:
from typing import Annotated
from annotated_types import MinLen
from typing_extensions import TypedDict
from pydantic import TypeAdapter
class Foobar(TypedDict, total=False):
a: int
b: Annotated[str, MinLen(5)]
ta = TypeAdapter(Foobar)
v = ta.validate_json(
'{"a": 1, "b": "12', experimental_allow_partial=True # (1)!
)
print(v)
#> {'a': 1}
v = ta.validate_json(
'{"a": 1, "b": "12"}', experimental_allow_partial=True # (2)!
)
print(v)
#> {'a': 1}
- 这将按预期通过验证,尽管最后一个字段会因验证失败而被省略。
- 这也将通过验证,因为传递给 pydantic-core 的 JSON 数据的二进制表示与前一种情况无法区分。
from typing import Annotated
from annotated_types import MinLen
from typing import TypedDict
from pydantic import TypeAdapter
class Foobar(TypedDict, total=False):
a: int
b: Annotated[str, MinLen(5)]
ta = TypeAdapter(Foobar)
v = ta.validate_json(
'{"a": 1, "b": "12', experimental_allow_partial=True # (1)!
)
print(v)
#> {'a': 1}
v = ta.validate_json(
'{"a": 1, "b": "12"}', experimental_allow_partial=True # (2)!
)
print(v)
#> {'a': 1}
- 这将按预期通过验证,尽管最后一个字段会因验证失败而被省略。
- 这也将通过验证,因为传递给 pydantic-core 的 JSON 数据的二进制表示与前一种情况无法区分。
输入最后一个字段中的任何错误都将被忽略¶
如上文所述,截断输入可能导致许多错误。Pydantic 并没有试图专门忽略可能由截断导致的错误,而是在部分验证模式下忽略输入最后一个元素中的所有错误。
这意味着如果错误发生在输入的最后一个字段中,明显无效的数据也会通过验证。
from typing import Annotated
from annotated_types import Ge
from pydantic import TypeAdapter
ta = TypeAdapter(list[Annotated[int, Ge(10)]])
v = ta.validate_python([20, 30, 4], experimental_allow_partial=True) # (1)!
print(v)
#> [20, 30]
ta = TypeAdapter(list[int])
v = ta.validate_python([1, 2, 'wrong'], experimental_allow_partial=True) # (2)!
print(v)
#> [1, 2]
- 正如您所料,这将通过验证,因为 Pydantic 正确地忽略了(被截断的)最后一项中的错误。
- 这也将通过验证,因为最后一项中的错误被忽略了。
验证可调用对象的参数¶
Pydantic 提供了 @validate_call
装饰器来对可调用对象提供的参数(以及返回类型)进行验证。然而,它只允许通过实际调用被装饰的可调用对象来提供参数。在某些情况下,您可能只想*验证*参数,例如从 JSON 数据等其他数据源加载时。
因此,可以使用实验性的 generate_arguments_schema()
函数来构建一个核心模式(core schema),该模式稍后可以与 SchemaValidator
一起使用。
from pydantic_core import SchemaValidator
from pydantic.experimental.arguments_schema import generate_arguments_schema
def func(p: bool, *args: str, **kwargs: int) -> None: ...
arguments_schema = generate_arguments_schema(func=func)
val = SchemaValidator(arguments_schema, config={'coerce_numbers_to_str': True})
args, kwargs = val.validate_json(
'{"p": true, "args": ["arg1", 1], "kwargs": {"extra": 1}}'
)
print(args, kwargs) # (1)!
#> (True, 'arg1', '1') {'extra': 1}
-
如果您希望将验证后的参数作为字典,可以使用
Signature.bind()
方法:from inspect import signature signature(func).bind(*args, **kwargs).arguments #> {'p': True, 'args': ('arg1', '1'), 'kwargs': {'extra': 1}}
注意
与 @validate_call
不同,这个核心模式只会验证提供的参数;底层的可调用对象将*不会*被调用。
此外,您可以通过提供一个回调函数来忽略特定参数,该回调函数会对每个参数被调用:
from typing import Any
from pydantic_core import SchemaValidator
from pydantic.experimental.arguments_schema import generate_arguments_schema
def func(p: bool, *args: str, **kwargs: int) -> None: ...
def skip_first_parameter(index: int, name: str, annotation: Any) -> Any:
if index == 0:
return 'skip'
arguments_schema = generate_arguments_schema(
func=func,
parameters_callback=skip_first_parameter,
)
val = SchemaValidator(arguments_schema)
args, kwargs = val.validate_json('{"args": ["arg1"], "kwargs": {"extra": 1}}')
print(args, kwargs)
#> ('arg1',) {'extra': 1}
MISSING
哨兵值¶
MISSING
哨兵值是一个单例,表示在验证期间未提供字段值。
当 None
有明确含义时,这个单例可以用作默认值,作为 None
的替代方案。在序列化期间,任何值为 MISSING
的字段都会从输出中排除。
from typing import Union
from pydantic import BaseModel
from pydantic.experimental.missing_sentinel import MISSING
class Configuration(BaseModel):
timeout: Union[int, None, MISSING] = MISSING
# configuration defaults, stored somewhere else:
defaults = {'timeout': 200}
conf = Configuration()
# `timeout` is excluded from the serialization output:
conf.model_dump()
# {}
# The `MISSING` value doesn't appear in the JSON Schema:
Configuration.model_json_schema()['properties']['timeout']
#> {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'title': 'Timeout'}}
# `is` can be used to discriminate between the sentinel and other values:
timeout = conf.timeout if conf.timeout is not MISSING else defaults['timeout']
from pydantic import BaseModel
from pydantic.experimental.missing_sentinel import MISSING
class Configuration(BaseModel):
timeout: int | None | MISSING = MISSING
# configuration defaults, stored somewhere else:
defaults = {'timeout': 200}
conf = Configuration()
# `timeout` is excluded from the serialization output:
conf.model_dump()
# {}
# The `MISSING` value doesn't appear in the JSON Schema:
Configuration.model_json_schema()['properties']['timeout']
#> {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'title': 'Timeout'}}
# `is` can be used to discriminate between the sentinel and other values:
timeout = conf.timeout if conf.timeout is not MISSING else defaults['timeout']
此功能被标记为实验性,因为它依赖于草案 PEP 661,该草案旨在在标准库中引入哨兵值。
因此,目前存在以下限制:
- 只有 Pyright 1.1.402 或更高版本支持对哨兵值的静态类型检查,并且应启用
enableExperimentalFeatures
类型评估设置。 - 不支持对包含
MISSING
值的模型进行序列化(pickling)。