[英]How to validate based on specific Enum member in a Fastapi Pydantic model
Here is my Pydantic model:这是我的 Pydantic model:
from enum import Enum
from pydantic import BaseModel
class ProfileField(str, Enum):
mobile = "mobile"
email = "email"
address = "address"
class ProfileType(str, Enum):
primary = "primary"
secondary = "secondary"
class ProfileDetail(BaseModel):
name: ProfileField
value: str
type: ProfileType
My API is accepting this type of JSON and its working fine.我的 API 正在接受这种类型的 JSON 并且工作正常。
{
"data": [
{
"name": "email",
"value": "abcd@gmail.com",
"type": "primary"
}
]
}
The requirement is email
is string type and needs a regex, mobile
is integer type and also needs a regex, and address
is a string and needs to be restricted to 50 characters.要求是email
是string类型需要regex, mobile
是integer类型也需要regex, address
是string需要限制在50个字符以内。
Is it possible to add corresponding validations?是否可以添加相应的验证?
If I understand correctly, the actual JSON data you receive has the top-level data
key and its value is an array of objects that you currently represent with your ProfileDetail
schema.如果我理解正确,您收到的实际 JSON 数据具有顶级data
键,其值是您当前使用ProfileDetail
模式表示的对象数组。
If that is the case, you may be better served by not using an Enum
at all for your name
field and instead defining a discriminated union based on the value of the name
field.如果是这种情况,您可能会更好地为您的name
字段完全不使用Enum
,而是根据name
字段的值定义一个可区分的联合。 You can write a separate model for each case ( mobile
, email
, and address
) and delegate validation to each of them for their own case.您可以为每个案例( mobile
、 email
和address
)编写一个单独的 model ,并将验证委托给每个案例。
Since all three of them share a base schema, you can define a base model for them to inherit from to reduce repetition.由于它们三个共享一个基础架构,因此您可以定义一个基础 model 供它们继承以减少重复。 The type
field for example can stay an Enum
(Pydantic handles validation of those out of the box ) and can be inherited by the three submodels.例如, type
字段可以保留为Enum
(Pydantic 处理开箱即用的验证)并且可以由三个子模型继承。
For mobile
and address
it sounds like you can just use constr
to define your constraints via the regex
and max_length
parameters respectively.对于mobile
和address
,听起来您可以使用constr
分别通过regex
和max_length
参数来定义约束。
For email
, you can use the built-in Pydantic type EmailStr
(subtype of str
).对于email
,您可以使用内置的 Pydantic 类型EmailStr
( str
的子类型)。 You'll just need to install the optional dependency with pip install 'pydantic[email]'
.您只需要使用pip install 'pydantic[email]'
安装可选的依赖项。
That way you should not even need to write any custom validators.这样你甚至不需要编写任何自定义验证器。
Here is the setup I suggest:这是我建议的设置:
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]
Let's test it with some fixtures:让我们用一些固定装置来测试它:
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",
}
The first six should be self explanatory.前六个应该是不言自明的。 test_data_invalid_name
should cause an error because "foo"
is not a valid discriminator value for name
. test_data_invalid_name
应该会导致错误,因为"foo"
不是name
的有效鉴别器值。 test_data_invalid_type
should demonstrate the built-in enum validator catching the invalid type
value "bar"
. test_data_invalid_type
应该演示捕获无效type
值"bar"
的内置枚举验证器。
Let's test the valid data first:我们先测试有效数据:
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))
...
Output: Output:
{
"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"
}
]
}
No surprises here.这里没有惊喜。 Now test those that should not pass the value
validation:现在测试那些不应该通过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))
...
Output: Output:
[
{
"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
}
}
]
Caught all the wrong values.捕获了所有错误的值。 Now just to be sure, the last two fixtures:现在可以肯定的是,最后两个固定装置:
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))
Output: Output:
[
{
"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"
]
}
}
]
Seems like we get the desired behavior from our model.似乎我们从 model 获得了所需的行为。
If you really want a separate model like the ProfileDetail
you showed in your question, that will not be possible with discriminated unions because those rely on being defined for a field on a separate model. In that case you'll actually have to write a custom validator (probably a root_validator
) to ensure consistency between name
and value
.如果您真的想要一个单独的 model,就像您在问题中显示的ProfileDetail
一样,这对于受歧视的联合来说是不可能的,因为它们依赖于为单独的 model 上的字段定义。在这种情况下,您实际上必须编写自定义验证器(可能是root_validator
)以确保name
和value
之间的一致性。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.