如何让 Pydantic 区分 List[Union[TypeA, TypeB]] 中的字段?

How To Get Pydantic To Discriminate On A Field Within List[Union[TypeA, TypeB]]?

我正在尝试使用 Pydantic 验证 POST 请求负载以获取 Rest API。申请人列表可以包含主要和可选的其他申请人。到目前为止,我已经编写了下面列出的以下 Pydantic 模型,以尝试反映这一点。其余 API json 有效负载使用布尔字段 isPrimary 来区分主要申请人和其他申请人。

from datetime import date
from pydantic import BaseModel, validator
from typing import List, Literal, Optional, Union


class PrimaryApplicant(BaseModel):
    isPrimary: Literal[True]
    dateOfBirth: Optional[date]


class OtherApplicant(BaseModel):
    isPrimary: Literal[False]
    dateOfBirth: date
    relationshipStatus: Literal["family", "friend", "other", "partner"]


class Application(BaseModel):
    applicants: List[Union[PrimaryApplicant, OtherApplicant]]

    @validator("applicants")
    def validate(
        cls,
        v: List[Union[PrimaryApplicant, OtherApplicant]]
    ) -> List[Union[PrimaryApplicant, OtherApplicant]]:

        list_count = len(v)
        primary_count = len(
            list(
                filter(lambda item: item.isPrimary, v)
            )
        )
        secondary_count = list_count - primary_count

        if primary_count > 1:
            raise ValueError("Only one primary applicant required")

        if secondary_count > 1:
            raise ValueError("Only one secondary applicant allowed")

        return v


def main() -> None:
    data_dict = {
        "applicants": [
            {
                "isPrimary": True
            },
            {
                "isPrimary": False,
                "dateOfBirth": date(1990, 1, 15),
                "relationshipStatus": "family"
            },
        ]
    }

    _ = Application(**data_dict)


if __name__ == "__main__":
    main()

对于上面列出的示例 json 有效载荷,当我尝试从 OtherApplicant 有效载荷中删除一些必需的强制字段时,正确地引发了 ValidationError。例如,如果我尝试删除 relationshipStatus 或 dateOfBirth 字段,则会引发错误。但是,isPrimary 字段也被 Pydantic 报告为无效。 Pydantic 认为 isPrimary 字段应该是 True???下面列出了示例 Pydantic 验证输出。

为什么 Pydantic 期望 json 负载中的 OtherApplicant 列表项的 isPrimary 字段应该为 True?由于使用 Union,它是否以某种方式将有效负载与 PrimaryApplicant 相关联?如果是这样,我如何让 Pydantic 使用 isPrimary 字段来区分列表有效负载中的主要申请人和其他申请人?

OtherApplicant 的列表负载中缺少 relationshipStatus 字段

pydantic.error_wrappers.ValidationError: 2 validation errors for Application
applicants -> 1 -> isPrimary
  unexpected value; permitted: True (type=value_error.const; given=False; permitted=(True,))
applicants -> 1 -> dateOfBirth
  field required (type=value_error.missing)

OtherApplicant 的列表负载中缺少出生日期字段

pydantic.error_wrappers.ValidationError: 2 validation errors for Application
applicants -> 1 -> isPrimary
  unexpected value; permitted: True (type=value_error.const; given=False; permitted=(True,))
applicants -> 1 -> relationshipStatus
  field required (type=value_error.missing)

通过在 Pydantic GitHub Repository

上提问找到了答案

Pydantic 1.9 引入了 discriminatory union 的概念。

升级到 Pydantic 1.9 并添加后:

Applicant = Annotated[
    Union[PrimaryApplicant, OtherApplicant],
    Field(discriminator="isPrimary")]

现在可以在我的 Application 模型中包含 applicants: List[Applicant] 字段。 isPrimary 字段被标记为用于区分主要申请人和其他申请人。

因此,完整的代码清单是:

from datetime import date
from pydantic import BaseModel, Field, validator
from typing import List, Literal, Optional, Union
from typing_extensions import Annotated


class PrimaryApplicant(BaseModel):
    isPrimary: Literal[True]
    dateOfBirth: Optional[date]


class OtherApplicant(BaseModel):
    isPrimary: Literal[False]
    dateOfBirth: date
    relationshipStatus: Literal["family", "friend", "other", "partner"]


Applicant = Annotated[
    Union[PrimaryApplicant, OtherApplicant],
    Field(discriminator="isPrimary")]


class Application(BaseModel):
    applicants: List[Applicant]

    @validator("applicants")
    def validate(cls, v: List[Applicant]) -> List[Applicant]:

        list_count = len(v)
        primary_count = len(
            list(
                filter(lambda item: item.isPrimary, v)
            )
        )
        secondary_count = list_count - primary_count

        if primary_count > 1:
            raise ValueError("Only one primary applicant required")

        if secondary_count > 1:
            raise ValueError("Only one secondary applicant allowed")

        return v


def main() -> None:
    data_dict = {
        "applicants": [
            {
                "isPrimary": True
            },
            {
                "isPrimary": False,
                "relationshipStatus": "family"
            },
        ]
    }

    _ = Application(**data_dict)


if __name__ == "__main__":
    main()