如何验证 Pydantic 模型的多个字段?

35
我想验证 Pydantic 模型的三个模型字段。为此,我正在从 pydantic 导入 root_validator,但是我遇到了以下错误:
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:\Users\Lenovo\AppData\Local\Programs\Python\Python38-32\lib\site-packages\pydantic\__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.

如果 from pydantic import root_validator 引发了一个 ImportError 错误,那么很可能是因为你没有正确的 pydantic 版本... 你使用的是哪个版本? - smarie
pydantic==0.32.2 - samba
最新版本为1.5.1...https://pypi.org/project/pydantic/ - smarie
5个回答

36

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)(参数名称valuesfieldconfig必须匹配),其中字段的值可以使用字段名称作为键来访问(例如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)

你会如何验证特定类型的所有字段?我知道@validator()装饰器有一个 *输入,可以将该验证器用于所有字段。但是,我想使用一个特定的验证器来验证所有浮点数。 - Curtwagner1984
1
由于Python是动态类型语言,我认为您需要使用验证器来验证所有字段 @validator("*") 并在内部进行类型检查 isinstance(value, float)。如果类型是 float,则进行验证,否则返回输入参数的返回值。但是,此验证将激活所有类型为float的输入,而与类声明中的类型无关。 - wueli
谢谢,这很有道理。 - Curtwagner1984
3
我认为您不需要@classmethod装饰器,因为@validator已经返回了一个类方法; 请参阅此问题 - Rich Inman

9
你需要将字段作为装饰器的参数传递。
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):

24
如果您解释一下您提供的代码如何回答问题,那么这将是一个更好的答案。 - pppery

6

选项1 - 使用 @validator装饰器

根据文档,“可以通过将多个字段名称传递给单个验证器来将其应用于多个字段”(并且“还可以通过传递特殊值'*'来调用所有字段)。因此,您可以将要验证的字段添加到验证器装饰器中,并使用field.name属性每次调用验证器时检查要验证的字段。如果字段未通过验证,则可以raise ValueError,“这将被捕获并用于填充ValidationError”(请参见此处的“注意”部分)。如果您需要基于其他字段验证字段,则必须首先使用values.get()方法检查它们是否已经过验证,如此答案(更新2)所示。以下演示了一个示例,其中验证诸如namecountry_code和电话号码(基于提供的country_code)等字段。提供的正则表达式模式仅用于此演示目的,并基于答案。
from pydantic import BaseModel, validator, ValidationError
import re

name_pattern = re.compile(r'[a-zA-Z\s]+$')
country_codes = {"uk", "us"}
UK_phone_pattern = re.compile(r'^(\+44\s?7\d{3}|\(?07\d{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

选项2 - 使用@root_validator装饰器

另一种方法是使用@root_validator,它允许对整个模型的数据进行验证。

from pydantic import BaseModel, root_validator, ValidationError
import re

name_pattern = re.compile(r'[a-zA-Z\s]+$')
country_codes = {"uk", "us"}
UK_phone_pattern = re.compile(r'^(\+44\s?7\d{3}|\(?07\d{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

5

首先,如果您在导入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'}

-1

这个例子包含了回答你问题所需的所有必要信息。

    class User(BaseModel):
        name: Optional[str] = ""

        class Config:
            validate_assignment = True

        @validator("name")
            def set_name(cls, name):
            return name or "foo"

3
目前你的回答不够清晰。请编辑并添加更多细节,以帮助他人了解如何回答提出的问题。有关如何撰写良好答案的更多信息,请在帮助中心查找。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接