Skip to content

实验性功能

在本节中,你将找到 Pydantic 中新的实验性功能的文档。这些功能可能会变更或移除,在将它们作为 Pydantic 的永久部分之前,我们正在寻求反馈和建议。

有关实验性功能的更多信息,请参阅我们的版本策略

反馈

我们欢迎对实验性功能的反馈!请在 Pydantic GitHub 仓库 上提交 issue 来分享你的想法、请求或建议。

我们也鼓励你阅读现有的反馈,并将你的想法添加到现有的 issue 中。

导入时的警告

当你从 experimental 模块导入实验性功能时,你会看到一条警告消息,提示该功能是实验性的。你可以通过以下方式禁用此警告:

import warnings

from pydantic import PydanticExperimentalWarning

warnings.filterwarnings('ignore', category=PydanticExperimentalWarning)

管道 API

Pydantic v2.8.0 引入了一个实验性的“管道”API,它允许以比现有 API 更类型安全的方式组合解析(验证)、约束和转换。此 API 可能会变更或移除,在将其作为 Pydantic 的永久部分之前,我们正在寻求反馈和建议。

API 文档

pydantic.experimental.pipeline

通常,管道 API 用于定义在验证期间应用于传入数据的一系列步骤。管道 API 设计为比现有的 Pydantic API 更类型安全和可组合。

管道中的每个步骤可以是:

  • 对提供的类型运行 pydantic 验证的验证步骤
  • 修改数据的转换步骤
  • 根据条件检查数据的约束步骤
  • 根据条件检查数据并在返回 False 时引发错误的断言步骤

请注意,以下示例试图以复杂性为代价做到详尽无遗:如果你发现自己在类型注解中编写这么多转换,你可能需要考虑使用 UserInUserOut 模型(如下例所示)或类似的方式,通过符合习惯的普通 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)!
    ]
  1. 将字符串转换为小写。
  2. 约束整数大于零。
  3. 约束字符串匹配正则表达式模式。
  4. 你也可以使用较低级别的转换、约束和断言方法。
  5. 使用 |& 运算符组合步骤(类似于逻辑 OR 或 AND)。
  6. 使用 Ellipsis... 作为第一个位置参数调用 validate_as(...) 意味着 validate_as(<字段类型>)。使用 validate_as(Any) 来接受任何类型。
  7. 你可以在其他步骤之前或之后调用 validate_as() 进行预处理或后处理。

BeforeValidatorAfterValidatorWrapValidator 映射

validate_as 方法是定义 BeforeValidatorAfterValidatorWrapValidator 的更类型安全的方式:

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)!
]
  1. 在将字符串解析为整数之前去除其空白字符。
  2. 在解析整数后将其乘以 2。
  3. 去除字符串的空白字符,将其验证为整数,然后乘以 2。

替代模式

根据场景不同,有许多替代模式可以使用。 举个例子,考虑上面提到的 UserInUserOut 模式:

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 的输出时特别有用,当模型流式传输结构化响应时,你可能希望在仍在接收数据时开始验证流(例如,向用户显示部分数据)。

Warning

部分验证是一个实验性功能,可能在未来的 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'}]
  1. TypedDict Foobar 有三个字段,但只有 a 是必需的,这意味着即使 bc 字段缺失,也可以创建有效的 Foobar 实例。
  2. 解析 JSON,输入在字符串被截断的位置之前是有效的 JSON。
  3. 在这种情况下,输入的截断意味着 c 的值(abcd)作为 c 字段的输入无效,因此被省略。
  4. a 字段是必需的,因此列表中唯一项的验证失败并被丢弃。
  5. 部分验证也适用于 Python 对象,它应该具有与 JSON 相同的语义,当然你不能有一个真正“不完整”的 Python 对象。
  6. 与上面相同,但使用 Python 对象,c 因为不是必需的且验证失败而被丢弃。
  7. trailing-strings 模式允许在部分 JSON 的末尾包含不完整的字符串,在这种情况下,输入在字符串被截断的位置之前是有效的 JSON,因此最后一个字符串被包含。

部分验证的工作原理

部分验证遵循 Pydantic 的禅意——它不保证输入数据可能是什么,但保证返回你所要求类型的有效实例,或引发验证错误。

为此,experimental_allow_partial 标志启用两种行为:

1. 部分 JSON 解析

Pydantic 使用的 jiter JSON 解析器已经支持解析部分 JSON, experimental_allow_partial 只是通过 allow_partial 参数传递给 jiter。

Note

如果你只想要支持部分 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 — 只有非必需字段可能缺失,例如通过 NotRequiredtotal=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]
    """
  1. b 的列表验证器没有从模型验证器那里接收到传递下来的 allow_partial 指令,因此它不知道忽略输入最后一个元素中的错误。

一些无效但完整的 JSON 将被接受

jiter(Pydantic 使用的 JSON 解析器)的工作方式意味着目前无法区分像 {"a": 1, "b": "12"} 这样的完整 JSON 和像 {"a": 1, "b": "12 这样的不完整 JSON。

这意味着在使用 experimental_allow_partial 时,一些无效的 JSON 将被 Pydantic 接受,例如:

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}
  1. 这将通过验证,正如预期的那样,尽管最后一个字段将因验证失败而被省略。
  2. 这也将通过验证,因为传递给 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}
  1. 这将通过验证,正如预期的那样,尽管最后一个字段将因验证失败而被省略。
  2. 这也将通过验证,因为传递给 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]
  1. 正如你所期望的,这将通过验证,因为 Pydantic 正确地忽略了(截断的)最后一项中的错误。
  2. 这也将通过验证,因为最后一项中的错误被忽略。

可调用对象参数的验证

Pydantic 提供了 @validate_call 装饰器来对提供的参数(以及返回类型)执行验证。但是,它只允许通过实际调用装饰后的可调用对象来提供参数。在某些情况下,你可能只想验证参数,例如从其他数据源(如 JSON 数据)加载时。

为此,实验性的 generate_arguments_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}
  1. 如果你想要经过验证的参数作为字典,你可以使用 Signature.bind() 方法:

    from inspect import signature
    
    signature(func).bind(*args, **kwargs).arguments
    #> {'p': True, 'args': ('arg1', '1'), 'kwargs': {'extra': 1}}
    

Note

@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


# 配置默认值,存储在其他地方:
defaults = {'timeout': 200}

conf = Configuration()

# `timeout` 从序列化输出中排除:
conf.model_dump()
# {}

# `MISSING` 值不会出现在 JSON Schema 中:
Configuration.model_json_schema()['properties']['timeout']
#> {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'title': 'Timeout'}}


# 可以使用 `is` 来区分标记和其他值:
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


# 配置默认值,存储在其他地方:
defaults = {'timeout': 200}

conf = Configuration()

# `timeout` 从序列化输出中排除:
conf.model_dump()
# {}

# `MISSING` 值不会出现在 JSON Schema 中:
Configuration.model_json_schema()['properties']['timeout']
#> {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'title': 'Timeout'}}


# 可以使用 `is` 来区分标记和其他值:
timeout = conf.timeout if conf.timeout is not MISSING else defaults['timeout']

此功能被标记为实验性,因为它依赖于草案 PEP 661,该草案在标准库中引入了标记。

因此,目前适用以下限制:

  • 标记的静态类型检查仅支持 Pyright 1.1.402 或更高版本,并且应启用 enableExperimentalFeatures 类型评估设置。
  • 不支持包含 MISSING 作为值的模型的序列化。