跳转至

pydantic 数据校验(v1)

本文内容适用于 pydantic v1.x

安装

Bash
pip install pydantic
image.png

基本使用

假定我们有一个基于pydantic的模型类如下:

Python
1
2
3
4
5
from pydantic import BaseModel


class MyModel(BaseModel):
    x: Type  # Type描述详见下面的表格
pydantic描述的数据有如下几种类型
image.png

数据校验

基本校验方式

下面的代码用来校验月份数值是否在1到12之间,不牵扯其他字段,直接使用validator来校验即可

Python
import pydantic


class A(pydantic.BaseModel):
    month: int

    @pydantic.validator('month')
    def validate_month(cls, v):
        if not 1 <= v <= 12:
            raise ValueError(f"month value must be in [1, 12]")
        return v


a1 = A(month=15)
运行后会抛出ValueError异常,即校验失败
image.png

基于A字段校验B字段

pydantic中,如果要校验多个字段field,可以导入模型的全部属性解决,使用root_validator可以使用自定义规则校验模型的全部字段

Python
import typing

import pydantic


class A(pydantic.BaseModel):
    month: int
    months: typing.List[int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]

    @pydantic.root_validator
    def validate_month(cls, values):
        if values['month'] not in values['months']:
            raise ValueError(f"month value must be in [1, 12]")
        return values


a1 = A(month=15)
image.png

额外字段校验

对于pydantic中没有定义的字段,默认忽略不解析。但另一方面,也可以使用Extra类来管理,设置为 Extra.forbid 即可,参考Field Types - Pydantic

Python
"""
pydantic 1.x
"""
from pydantic import BaseModel, Extra


class Person(BaseModel):
    name: str
    age: int

    class Config:
        extra = Extra.forbid  # 不允许多余字段


p1 = Person(name='John', age=30)  # OK

p2 = Person(name='John', age=30, country='USA')  # will raise ValidationError

# pydantic_core._pydantic_core.ValidationError: 1 validation error for Person
# country
#   Extra inputs are not permitted [type=extra_forbidden, input_value='USA', input_type=str]

模型类使用

序列化与实例化

pydantic.BaseModel可以非常容易地进行对象的序列化,可以很方便地转成Python的dict类型和标准的json字符串,示例如下

Python
"""
pydantic 1.x
"""
import json
import typing

from pydantic import BaseModel


class A(BaseModel):
    x: int
    y: typing.Optional[str]
    z: typing.Optional[list] = [1, 2, 3]


a = A(x=1)

ad = a.dict()

aj = a.json()
ja = json.loads(aj)

aa = A.parse_obj(ad)
image.png

对可选字段的对象反序列化注意

在上面的实例中,对于模型A的字段y,如果实例化时没有赋值,那么实例化后的对象中y=None,完整字典形式为ad={'x': 1, 'y': None, 'z': [1, 2, 3]},此时不能将字典ad直接使用A.__init__进行反序列化,因为在模型类A中,只允许str类型的可选字段y

获取模型类中带有默认属性值的字段

Python
from typing import Optional

from pydantic import BaseModel


class MyModel(BaseModel):
    a: int = 1
    b: Optional[str]
    c: list

    @classmethod
    def default_attrs(cls):
        properties = cls.schema()['properties']
        return {k: v['default'] for k, v in properties.items() if v.get('default') is not None}


x = MyModel.default_attrs()  # {'a': 1}

单元测试

在Python标准库unittest中,无法直接mock掉基于pydantic创建的对象方法。
image.png
play_pydantic/src.py

Python
import pydantic


class A(pydantic.BaseModel):
    month: int

    @pydantic.validator('month')
    def validate_month(cls, v):
        if not 1 <= v <= 12:
            raise ValueError(f"month value must be in [1, 12]")
        return v

    def get_next_month(self):
        return self.month + 1 if self.month < 12 else 1
play_pydantic/test.py
直接mock.Mock()将会报错
Python
import unittest
from unittest import mock

from play_pydantic.src import A


class TestA(unittest.TestCase):
    def test_a_cannot_be_mocked(self):
        a = A(month=1)
        a.get_next_month = mock.Mock()


if __name__ == "__main__":
    unittest.main()
image.png
此时其实可以使用mock.patch()解决即可
Python
import unittest
from unittest import mock

from play_pydantic.src import A


class TestA(unittest.TestCase):
    def test_a_can_be_patched(self):
        with mock.patch("play_pydantic.src.A.get_next_month") as fake_fun:
            fake_fun.return_value = 0
            a = A(month=2)
            self.assertEqual(0, a.get_next_month())


if __name__ == "__main__":
    unittest.main()

参考