如何在Fastapi Pydantic模型中基于特定Enum成员进行验证



这是我的Pydantic模型:

from enum import Enum
from pydantic import BaseModel

class ProfileField(str, Enum):
mobile = "mobile"
email = "email"
address = "address"
interests ="interests"  # need list of strings

class ProfileType(str, Enum):
primary = "primary"
secondary = "secondary"

class ProfileDetail(BaseModel):
name: ProfileField
value: str
type: ProfileType

我的API正在接受这种类型的JSON,并且工作正常。

{
"data": [
{
"name": "email",
"value": "abcd@gmail.com",
"type": "primary"
}
]
}

要求email是字符串类型,需要一个正则表达式,mobile是整数类型,也需要一个正则表达式,address是字符串类型,需要限制为50个字符。

是否可以添加相应的验证?

鉴别联合和内置类型/验证器

如果我理解正确,您收到的实际JSON数据具有顶级data键,其值是您当前用ProfileDetail模式表示的对象数组。

如果是这种情况,最好不要为name字段使用Enum,而是根据name字段的值定义一个有区别的联合。您可以为每种情况(mobileemailaddress)编写一个单独的模型,并将验证委托给它们各自的情况。

由于它们三个共享一个基本模式,您可以为它们定义一个基本模型来继承,以减少重复。例如,type字段可以保持为Enum(Pydantic处理开箱外的验证),并且可以由三个子模型继承。

对于mobileaddress,听起来你可以使用constr分别通过regexmax_length参数定义你的约束。

对于email,可以使用内置的Pydantic类型EmailStr(str的子类型)。你只需要安装pip install 'pydantic[email]'的可选依赖项。

这样你甚至不需要编写任何自定义验证器。

以下是我建议的设置:
from enum import Enum
from typing import Annotated, Literal, Union
from pydantic import BaseModel, EmailStr, Field, constr
class ProfileType(str, Enum):
primary = "primary"
secondary = "secondary"
class BaseProfileFieldData(BaseModel):
value: str
type: ProfileType
class MobileData(BaseProfileFieldData):
value: constr(regex=r"d{5,}")  # your actual regex here
name: Literal["mobile"]
class EmailData(BaseProfileFieldData):
value: EmailStr
name: Literal["email"]
class AddressData(BaseProfileFieldData):
value: constr(max_length=50)
name: Literal["address"]
ProfileField = Annotated[
Union[MobileData, EmailData, AddressData],
Field(discriminator="name")
]
class ProfileDetails(BaseModel):
data: list[ProfileField]

测试让我们用一些fixture来测试它:

test_data_mobile_valid = {
"name": "mobile",
"value": "123456",
"type": "secondary",
}
test_data_mobile_invalid = {
"name": "mobile",
"value": "12",
"type": "secondary",
}
test_data_email_valid = {
"name": "email",
"value": "abcd@gmail.com",
"type": "primary",
}
test_data_email_invalid = {
"name": "email",
"value": "abcd@gmail@..",
"type": "primary",
}
test_data_address_valid = {
"name": "address",
"value": "some street 42, 12345 example",
"type": "secondary",
}
test_data_address_invalid = {
"name": "address",
"value": "x" * 51,
"type": "secondary",
}
test_data_invalid_name = {
"name": "foo",
"value": "x",
"type": "primary",
}
test_data_invalid_type = {
"name": "mobile",
"value": "123456",
"type": "bar",
}

前六条应该是不言自明的。test_data_invalid_name应该导致错误,因为"foo"不是name的有效鉴别值。test_data_invalid_type应该演示内置的枚举验证器捕获无效的type"bar"

让我们先测试有效数据:

if __name__ == "__main__":
from pydantic import ValidationError
obj = ProfileDetails.parse_obj({
"data": [
test_data_mobile_valid,
test_data_email_valid,
test_data_address_valid,
]
})
print(obj.json(indent=4))
...

输出:

{
"data": [
{
"value": "123456",
"type": "secondary",
"name": "mobile"
},
{
"value": "abcd@gmail.com",
"type": "primary",
"name": "email"
},
{
"value": "some street 42, 12345 example",
"type": "secondary",
"name": "address"
}
]
}

没什么好惊讶的。现在测试那些不应该通过value验证的:

if __name__ == "__main__":
...
try:
ProfileDetails.parse_obj({
"data": [
test_data_mobile_invalid,
test_data_email_invalid,
test_data_address_invalid,
]
})
except ValidationError as exc:
print(exc.json(indent=4))
...

输出:

[
{
"loc": [
"data",
0,
"MobileData",
"value"
],
"msg": "string does not match regex "\d{5,}"",
"type": "value_error.str.regex",
"ctx": {
"pattern": "\d{5,}"
}
},
{
"loc": [
"data",
1,
"EmailData",
"value"
],
"msg": "value is not a valid email address",
"type": "value_error.email"
},
{
"loc": [
"data",
2,
"AddressData",
"value"
],
"msg": "ensure this value has at most 50 characters",
"type": "value_error.any_str.max_length",
"ctx": {
"limit_value": 50
}
}
]

捕获了所有错误的值。现在为了确定,最后两个fixture:

if __name__ == "__main__":
...
try:
ProfileDetails.parse_obj({
"data": [
test_data_invalid_name,
test_data_invalid_type,
]
})
except ValidationError as exc:
print(exc.json(indent=4))

输出:

[
{
"loc": [
"data",
0
],
"msg": "No match for discriminator 'name' and value 'foo' (allowed values: 'mobile', 'email', 'address')",
"type": "value_error.discriminated_union.invalid_discriminator",
"ctx": {
"discriminator_key": "name",
"discriminator_value": "foo",
"allowed_values": "'mobile', 'email', 'address'"
}
},
{
"loc": [
"data",
1,
"MobileData",
"type"
],
"msg": "value is not a valid enumeration member; permitted: 'primary', 'secondary'",
"type": "type_error.enum",
"ctx": {
"enum_values": [
"primary",
"secondary"
]
}
}
]

似乎我们从模型中得到了期望的行为。


警告

如果你真的想要一个单独的模型,就像你在问题中展示的ProfileDetail一样,这将是不可能的,因为那些依赖于为字段定义的鉴别联合。在一个单独的模型上。在这种情况下,您实际上必须编写一个自定义验证器(可能是root_validator)来确保namevalue之间的一致性。

最新更新