如何验证 Pydantic 模型的多个字段?
- 2025-03-04 08:25:00
- admin 原创
- 101
问题描述:
我想验证 Pydantic 模型的三个模型字段。为此,我root_validator
从 pydantic 导入,但出现以下错误:
from pydantic import BaseModel, ValidationError, root_validator
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: cannot import name 'root_validator' from 'pydantic' (C:UsersLenovoAppDataLocalProgramsPythonPython38-32libsite-packagespydantic__init__.py)
我尝试过这个:
@validator
def validate_all(cls, v, values, **kwargs):
...
我从父模型的一些公共字段继承了我的 pydantic 模型。值仅显示父类字段,但不显示我的子类字段。例如:
class Parent(BaseModel):
name: str
comments: str
class Customer(Parent):
address: str
phone: str
@validator
def validate_all(cls, v, values, **kwargs):
#here values showing only (name and comment) but not address and phone.
...
解决方案 1:
为了扩展的答案Rahul R
,此示例更详细地展示了如何使用pydantic
验证器。
此示例包含回答您的问题所需的所有信息。
请注意,还有使用 的选项@root_validator
,如 所述Kentgrav
,请参阅帖子底部的示例以了解更多详细信息。
import pydantic
class Parent(pydantic.BaseModel):
name: str
comments: str
class Customer(Parent):
address: str
phone: str
# If you want to apply the Validator to the fields "name", "comments", "address", "phone"
@pydantic.validator("name", "comments", "address", "phone")
@classmethod
def validate_all_fields_one_by_one(cls, field_value):
# Do the validation instead of printing
print(f"{cls}: Field value {field_value}")
return field_value # this is the value written to the class field
# if you want to validate to content of "phone" using the other fields of the Parent and Child class
@pydantic.validator("phone")
@classmethod
def validate_one_field_using_the_others(cls, field_value, values, field, config):
parent_class_name = values["name"]
parent_class_address = values["address"] # works because "address" is already validated once we validate "phone"
# Do the validation instead of printing
print(f"{field_value} is the {field.name} of {parent_class_name}")
return field_value
Customer(name="Peter", comments="Pydantic User", address="Home", phone="117")
输出
<class '__main__.Customer'>: Field value Peter
<class '__main__.Customer'>: Field value Pydantic User
<class '__main__.Customer'>: Field value Home
<class '__main__.Customer'>: Field value 117
117 is the phone number of Peter
Customer(name='Peter', comments='Pydantic User', address='Home', phone='117')
更详细地回答你的问题:
将需要验证的字段@validator
直接添加到验证函数上方的装饰器中。
@validator("name")
`"name"使用(例如)的字段值
"Peter"作为验证函数的输入。该类及其父类的所有字段都可以添加到
@validator`装饰器中。然后,验证函数 (
validate_all_fields_one_by_one
) 使用字段值作为第二个参数 (field_value
) 来验证输入。验证函数的返回值写入类字段。验证函数的签名是def validate_something(cls, field_value)
可以任意选择函数和变量名称的地方(但第一个参数应该是cls
)。根据 Arjan(https://youtu.be/Vj-iU-8_xLs?t=329),还@classmethod
应该添加装饰器。
如果目标是使用父类和子类的其他(已验证的)字段来验证一个字段,则验证函数的完整签名是def validate_something(cls, field_value, values, field, config)
(参数名称values
,field
并且config
必须匹配),其中可以使用字段名称作为键来访问字段的值(例如values["comments"]
)。
编辑1:如果您只想检查某种类型的输入值,则可以使用以下结构:
@validator("*") # validates all fields
def validate_if_float(cls, value):
if isinstance(value, float):
# do validation here
return value
编辑2:使用以下更简单的方法来验证所有字段@root_validator
:
import pydantic
class Parent(pydantic.BaseModel):
name: str
comments: str
class Customer(Parent):
address: str
phone: str
@pydantic.root_validator()
@classmethod
def validate_all_fields_at_the_same_time(cls, field_values):
# Do the validation instead of printing
print(f"{cls}: Field values are: {field_values}")
assert field_values["name"] != "invalid_name", f"Name `{field_values['name']}` not allowed."
return field_values
输出:
Customer(name="valid_name", comments="", address="Street 7", phone="079")
<class '__main__.Customer'>: Field values are: {'name': 'valid_name', 'comments': '', 'address': 'Street 7', 'phone': '079'}
Customer(name='valid_name', comments='', address='Street 7', phone='079')
Customer(name="invalid_name", comments="", address="Street 7", phone="079")
ValidationError: 1 validation error for Customer
__root__
Name `invalid_name` not allowed. (type=assertion_error)
解决方案 2:
选项 1 - 使用@validator
装饰器
根据文档,“通过向其传递多个字段名称,可以将单个字段validator
应用于多个字段”(并且“也可以通过传递特殊值在所有'*'
字段上调用”)。因此,您可以将要验证的字段添加到validator
装饰器中,并使用field.name
属性检查每次validator
调用时要验证哪一个。如果某个字段未通过验证,您可以raise ValueError
“捕获并用于填充”(请参阅此处的ValidationError
“注释”部分)。如果您需要根据其他字段验证字段,则必须首先检查它们是否已使用方法进行了验证,如本答案(更新 2)所示。下面演示了一个示例,其中验证了诸如、和数字(基于提供的)之类的字段。提供的正则表达式模式只是本演示的示例,并且基于此和此答案。values.get()
`namecountry_code
phone`country_code
from pydantic import BaseModel, validator
import re
name_pattern = re.compile(r'[a-zA-Zs]+$')
country_codes = {"uk", "us"}
UK_phone_pattern = re.compile(r'^(+44s?7d{3}|(?07d{3})?)s?d{3}s?d{3}$') # UK mobile phone number. Valid example: +44 7222 555 555
US_phone_pattern = re.compile(r'^(([0-9]{3}) |[0-9]{3}-)[0-9]{3}-[0-9]{4}$') # US phone number. Valid example: (123) 123-1234
phone_patterns = {"uk": UK_phone_pattern, "us": US_phone_pattern}
class Parent(BaseModel):
name: str
comments: str
class Customer(Parent):
address: str
country_code: str
phone: str
@validator('name', 'country_code', 'phone')
def validate_atts(cls, v, values, field):
if field.name == "name":
if not name_pattern.match(v): raise ValueError(f'{v} is not a valid name.')
elif field.name == "country_code":
if not v.lower() in country_codes: raise ValueError(f'{v} is not a valid country code.')
elif field.name == "phone" and values.get('country_code'):
c_code = values.get('country_code').lower()
if not phone_patterns[c_code].match(v): raise ValueError(f'{v} is not a valid phone number.')
return v
更新 - Pydantic V2 示例
在 Pydantic V2 中,@validator
已被弃用,并被 取代。如果你想从 内的另一个字段@field_validator
访问,这可以使用,它是字段名称到字段值的字典。values
`@field_validator`ValidationInfo.data
from pydantic import BaseModel, ValidationInfo, field_validator
import re
# ... the rest of the code is the same as above
class Customer(Parent):
address: str
country_code: str
phone: str
@field_validator('name', 'country_code', 'phone')
@classmethod
def validate_atts(cls, v: str, info: ValidationInfo):
if info.field_name == 'name':
if not name_pattern.match(v): raise ValueError(f'{v} is not a valid name.')
elif info.field_name == 'country_code':
if not v.lower() in country_codes: raise ValueError(f'{v} is not a valid country code.')
elif info.field_name == 'phone' and info.data.get('country_code'):
c_code = info.data.get('country_code').lower()
if not phone_patterns[c_code].match(v): raise ValueError(f'{v} is not a valid phone number.')
return v
选项 2 - 使用@root_validator
装饰器
另一种方法是使用@root_validator
,它允许对整个模型的数据进行验证。
from pydantic import BaseModel, root_validator
import re
name_pattern = re.compile(r'[a-zA-Zs]+$')
country_codes = {"uk", "us"}
UK_phone_pattern = re.compile(r'^(+44s?7d{3}|(?07d{3})?)s?d{3}s?d{3}$') # UK mobile phone number. Valid example: +44 7222 555 555
US_phone_pattern = re.compile(r'^(([0-9]{3}) |[0-9]{3}-)[0-9]{3}-[0-9]{4}$') # US phone number. Valid example: (123) 123-1234
phone_patterns = {"uk": UK_phone_pattern, "us": US_phone_pattern}
class Parent(BaseModel):
name: str
comments: str
class Customer(Parent):
address: str
country_code: str
phone: str
@root_validator()
def validate_atts(cls, values):
name = values.get('name')
comments = values.get('comments')
address = values.get('address')
country_code = values.get('country_code')
phone = values.get('phone')
if name is not None and not name_pattern.match(name):
raise ValueError(f'{name} is not a valid name.')
if country_code is not None and not country_code.lower() in country_codes:
raise ValueError(f'{country_code} is not a valid country code.')
if phone is not None and country_code is not None:
if not phone_patterns[country_code.lower()].match(phone):
raise ValueError(f'{phone} is not a valid phone number.')
return values
更新 - Pydantic V2 示例
在 Pydantic V2 中,@root_validator
已被弃用,并被 取代@model_validator
。模型验证器可以是mode='before'
,mode='after'
或mode='wrap'
。在这种情况下,mode='after'
最适合。如文档中所述:
mode='after'
验证器是实例方法,并且始终接收模型实例作为第一个参数。您不应将其用作(cls, ModelType)
签名,而应使用(self)
并让类型检查器为您推断类型self
。由于这些是完全类型安全的,因此它们通常比验证器更容易实现mode='before'
。如果任何字段验证失败,mode='after'
则不会调用该字段的验证器。
使用 mode='after'
from pydantic import BaseModel, model_validator
import re
# ... the rest of the code is the same as above
class Customer(Parent):
address: str
country_code: str
phone: str
@model_validator(mode='after')
def validate_atts(self):
name = self.name
comments = self.comments
address = self.address
country_code = self.country_code
phone = self.phone
if name is not None and not name_pattern.match(name):
raise ValueError(f'{name} is not a valid name.')
if country_code is not None and not country_code.lower() in country_codes:
raise ValueError(f'{country_code} is not a valid country code.')
if phone is not None and country_code is not None:
if not phone_patterns[country_code.lower()].match(phone):
raise ValueError(f'{phone} is not a valid phone number.')
return self
使用 mode='before'
如果您更愿意使用mode='before
,您可以按如下方式操作。请注意,在这种情况下,您应该自行检查字段值是否符合预期格式(例如,str
在下面的示例中),然后再进行进一步的处理/验证(例如,将值转换为小写、字符串值比较等)——下面未包括。
from pydantic import BaseModel, model_validator
from typing import Any
import re
# ... the rest of the code is the same as above
class Customer(Parent):
address: str
country_code: str
phone: str
@model_validator(mode='before')
@classmethod
def validate_atts(cls, data: Any):
if isinstance(data, dict):
name = data.get('name')
comments = data.get('comments')
address = data.get('address')
country_code = data.get('country_code')
phone = data.get('phone')
if name is not None and not name_pattern.match(name):
raise ValueError(f'{name} is not a valid name.')
if country_code is not None and not country_code.lower() in country_codes:
raise ValueError(f'{country_code} is not a valid country code.')
if phone is not None and country_code is not None:
if not phone_patterns[country_code.lower()].match(phone):
raise ValueError(f'{phone} is not a valid phone number.')
return data
选项 1 和 2 的测试示例
from pydantic import ValidationError
# should throw "Value error, (123) 123-1234 is not a valid phone number."
try:
Customer(name='john', comments='hi', address='some address', country_code='UK', phone='(123) 123-1234')
except ValidationError as e:
print(e)
# should work without errors
print(Customer(name='john', comments='hi', address='some address', country_code='UK', phone='+44 7222 555 555'))
解决方案 3:
您需要将字段作为装饰器的参数传递。
class Parent(BaseModel):
name: str
comments: str
class Customer(Parent):
address: str
phone: str
@validator("name", "coments", "address", "phone")
def validate_all(cls, v, values, **kwargs):
解决方案 4:
首先,如果您在导入root_validator时遇到错误,我会更新 pydantic。
pip install -U pydantic
上面的许多示例向您展示了如何一次对多个值使用同一个验证器。或者它们增加了很多不必要的复杂性来实现您想要的效果。您只需使用以下代码,即可使用root_validator装饰器在同一验证器中同时验证多个字段。:
from pydantic import root_validator
from pydantic import BaseModel
class Parent(BaseModel):
name: str = "Peter"
comments: str = "Pydantic User"
class Customer(Parent):
address: str = "Home"
phone: str = "117"
@root_validator
def validate_all(cls, values):
print(f"{values}")
values["phone"] = "111-111-1111"
values["address"] = "1111 Pydantic Lane"
print(f"{values}")
return values
Output:
{'name': 'Peter', 'comments': 'Pydantic User', 'address': 'Home', 'phone': '117'}
{'name': 'Peter', 'comments': 'Pydantic User', 'address': '1111 Pydantic Lane', 'phone': '111-111-1111'}
解决方案 5:
这里的许多答案都涉及如何将验证器添加到单个 pydantic 模型。我将添加如何在模型之间共享验证器 - 以及其他一些高级技术。
注意:以下内容适用于 Pydantic V2。
选项 1.装饰器中的多个字段validate(...)
:
先前对这个问题的回答提供了验证多个字段的最简单和最容易的方法 - 即向单个验证器提供多个字段名称:
...
@field_validator("field_1", "field_2", ...)
def my_validator(...):
...
现有的答案已经足够了,我建议阅读它们以进一步了解。
选项 2. 定义单个验证函数并在父级和子级之间重用:
这实际上允许您在整个项目中导入/重用验证器。
from pydantic import field_validator, BaseModel
def must_be_title_case(v: str) -> str:
"""Validator to be used throughout"""
if v != v.title():
raise ValueError("must be title cased")
return v
class Parent(BaseModel):
name: str = "Peter"
comments: str = "Pydantic User"
validate_fields = field_validator("name", "comments")(must_be_title_case)
class Customer(Parent):
address: str = "Home"
phone: str = "117"
validate_fields = field_validator("address", "phone")(must_be_title_case)
或者,如果您愿意,您可以仅为子项定义所有字段的字段验证:
class Parent(BaseModel):
name: str = "Peter"
comments: str = "Pydantic User"
class Customer(Parent):
address: str = "Home"
phone: str = "117"
validate_fields = field_validator("name", "comments", "address", "phone")(must_be_title_case)
选项 3. 将您的验证定义为带注释的验证器:
这使您可以定义可重复使用的经过验证的“类型” - 灵活性非常高:
from typing_extensions import Annotated
from pydantic import BaseModel, ValidationError, field_validator
from pydantic.functional_validators import AfterValidator
# Same function as before
def must_be_title_case(v: str) -> str:
"""Validator to be used throughout"""
if v != v.title():
raise ValueError("must be title cased")
return v
# Define your annotated (validated) type:
MySpecialString = Annotated[str, AfterValidator(must_be_title_case)]
# Now use the custom type in your models
class Customer(Parent):
address: MySpecialString = "Home"
phone: MySpecialString = "117"
class Parent(BaseModel):
name: MySpecialString = "Peter"
comments: MySpecialString = "Pydantic User"
稍微解释一下注释类型中发生的事情:
基类型是
string
Pydantic 将尝试将输入值强制转换为字符串。这被视为“核心验证”步骤
在 pydantic 的验证之后,我们将运行我们的验证器函数(由 声明
AfterValidator
)——如果成功,则设置返回的值。您也可以选择
BeforeValidator
在注释中声明,它将在 Pydantic 尝试强制值之前运行我们的函数。
选项 4.相互验证字段:
前面的方法展示了如何单独验证多个字段。但是如果你想比较两个值怎么办?
一个常见的例子是比较两个可选的日期值 - 如果两个都设置了,则确保一个大于另一个。我将在下面演示这一点:
比较多个字段的最佳方法是使用model_validator(又名 v1 中的 root_validator):
class MyModel(BaseModel):
date_1: Optional[datetime] = None
date_2: Optional[datetime] = None
@model_validator(mode="after")
def validate_dates(self):
"""Date 1 must always be larger than date 2, if they are both set"""
if self.date_1 and self.date_2:
if self.date_1 < self.date_2:
raise ValueError("date_2 cannot be larger than date_1")
return self
注意mode="after"
——这允许 pydantic 首先执行其自己的验证(将值强制转换为日期时间对象 + 设置默认值)。
从技术上讲,您可以使用字段验证器执行类似操作,但不能保证在验证时在模型上设置其他字段值 - 请参阅Pydantic 文档中的额外说明。
我希望这能为您设计解决方案提供足够的背景信息。
解决方案 6:
此示例包含回答您的问题所需的所有信息。
class User(BaseModel):
name: Optional[str] = ""
class Config:
validate_assignment = True
@validator("name")
def set_name(cls, name):
return name or "foo"
扫码咨询,免费领取项目管理大礼包!