Try โ€‚โ€‰HackMD

pydantic

RogelioKG/pydantic

References

Note

ๆญคๆ–‡ไปฅ่ƒฝๆœ€ๅฟซไธŠๆ‰‹ Pydantic ็‚บๅ„ชๅ…ˆ (ๅญธ็ฟ’ๅ…ฉๆˆๅŠŸ่ƒฝ๏ผŒๆปฟ่ถณๅ…ซๆˆ็”จ้€”) ็š„็ต„็น”็ตๆง‹ๆ’ฐๅฏซ๏ผŒ่‹ฅๆœ‰ไธๅšด่ฌนใ€ไธๅฎŒๅ–„ไน‹่™•่ซ‹่ฆ‹่ซ’ใ€‚

Brief

  • pydantic ็”จๆ–ผ่ณ‡ๆ–™้ฉ—่ญ‰
    • ๅˆฉ็”จ type hints ไพ†้€ฒ่กŒ้ฉ—่ญ‰
    • ไปฐ่ณด pydantic-core ๅบซ๏ผŒๅ…ถไฝฟ็”จ Rust ็ทจๅฏซ่€Œๆˆ๏ผŒ้€Ÿๅบฆ้ฃ›ๅฟซ
  • pydantic ็š„ optional dependencies
    • pydantic[email]๏ผš่™•็† email ๆฌ„ไฝ้ฉ—่ญ‰
    • pydantic[timezone]๏ผš่™•็† timezone ๆฌ„ไฝ้ฉ—่ญ‰

Models

BaseModel Model

  • ๅ…่จฑๅตŒๅฅ—
  • ๆ–นๆณ•
    • ้ฉ—่ญ‰ (่‹ฅๅ…่จฑ None๏ผŒModel ๅฑฌๆ€งๅž‹ๅˆฅ่ฆๅคšๆจ™่จป | None ๏ผŒไธฆ็ตฆ้ ่จญๅ€ผ None )
      • model_validate() (class method)
        • ่ผธๅ…ฅ๏ผšobject๏ผˆๅฏไปฅๆ˜ฏ dict ๆˆ–ๆ˜ฏ ORM ็‰ฉไปถ๏ผ‰
        • ่ผธๅ‡บ๏ผšModel ็‰ฉไปถ
      • model_validate_json() (class method)
        • ่ผธๅ…ฅ๏ผšๅญ—ไธฒ (JSON)
        • ่ผธๅ‡บ๏ผšModel ็‰ฉไปถ
    • ๅ€’ๅ‡บ (ไฝฟ็”จ exclude_unset=True ้Žๆฟพ None ๅ€ผ )
      • model_dump() (instance method)
        • ่ผธๅ‡บ๏ผšdict
      • model_dump_json() (instance method)
        • ่ผธๅ‡บ๏ผšๅญ—ไธฒ (JSON)
    • JSON Schema
      • model_json_schema() (class method)
        • ๅƒๆ•ธ
          • mode="validation" (่ผธๅ…ฅ็š„ JSON Schema)
            mode="serialization" (่ผธๅ‡บ็š„ JSON Schema)
        • ่ผธๅ‡บ๏ผšdict (JSON Schema)
  • ๆญ้… ORM
    โ€‹โ€‹โ€‹โ€‹with db.Session() as session:
    โ€‹โ€‹โ€‹โ€‹    emp = session.query(table.Employee).filter(table.Employee.emp_name == "RogelioKG").first()
    โ€‹โ€‹โ€‹โ€‹    print(schema.EmployeeModel.model_validate(emp).model_dump(mode="json"))
    โ€‹โ€‹โ€‹โ€‹    # model_validate ๆœƒๅ›žๅ‚ณ EmployeeModel ๅฏฆไพ‹
    โ€‹โ€‹โ€‹โ€‹    # ๅ†ๆญ้… model_dump ๅฐฑๅฏไปฅๆ‹ฟๅˆฐ dict ไบ†
    
  • ็ฏ„ไพ‹
    โ€‹โ€‹โ€‹โ€‹from datetime import datetime
    
    โ€‹โ€‹โ€‹โ€‹from pydantic import BaseModel, PositiveInt
    
    
    โ€‹โ€‹โ€‹โ€‹class User(BaseModel):
    โ€‹โ€‹โ€‹โ€‹    id: int
    โ€‹โ€‹โ€‹โ€‹    name: str = "John Doe"
    โ€‹โ€‹โ€‹โ€‹    signup_ts: datetime | None
    โ€‹โ€‹โ€‹โ€‹    tastes: dict[str, PositiveInt]
    
    
    โ€‹โ€‹โ€‹โ€‹def test_user():
    โ€‹โ€‹โ€‹โ€‹    external_data = {
    โ€‹โ€‹โ€‹โ€‹        "id": 123,
    โ€‹โ€‹โ€‹โ€‹        "signup_ts": "2019-06-01 12:22",
    โ€‹โ€‹โ€‹โ€‹        "tastes": {
    โ€‹โ€‹โ€‹โ€‹            "wine": 9,
    โ€‹โ€‹โ€‹โ€‹            b"cheese": 7,
    โ€‹โ€‹โ€‹โ€‹            "cabbage": "1",
    โ€‹โ€‹โ€‹โ€‹        },
    โ€‹โ€‹โ€‹โ€‹    }
    
    โ€‹โ€‹โ€‹โ€‹    user = User(**external_data)
    
    โ€‹โ€‹โ€‹โ€‹    print(type(user.signup_ts))
    โ€‹โ€‹โ€‹โ€‹    # <class 'datetime.datetime'>
    
    โ€‹โ€‹โ€‹โ€‹    print(user.model_dump())
    โ€‹โ€‹โ€‹โ€‹    # {
    โ€‹โ€‹โ€‹โ€‹    #     'id': 123,
    โ€‹โ€‹โ€‹โ€‹    #     'name': 'John Doe',
    โ€‹โ€‹โ€‹โ€‹    #     'signup_ts': datetime.datetime(2019, 6, 1, 12, 22),
    โ€‹โ€‹โ€‹โ€‹    #     'tastes': {'wine': 9, 'cheese': 7, 'cabbage': 1},
    โ€‹โ€‹โ€‹โ€‹    # }
    
    โ€‹โ€‹โ€‹โ€‹    print(user.model_dump_json())
    โ€‹โ€‹โ€‹โ€‹    # {
    โ€‹โ€‹โ€‹โ€‹    #     'id': 123,
    โ€‹โ€‹โ€‹โ€‹    #     'name': 'John Doe',
    โ€‹โ€‹โ€‹โ€‹    #     'signup_ts': '2019-06-01T12:22:00',
    โ€‹โ€‹โ€‹โ€‹    #     'tastes': {'wine': 9, 'cheese': 7, 'cabbage': 1},
    โ€‹โ€‹โ€‹โ€‹    # }
    
    โ€‹โ€‹โ€‹โ€‹    print(User.model_json_schema())
    โ€‹โ€‹โ€‹โ€‹    # {
    โ€‹โ€‹โ€‹โ€‹    #     "properties": {
    โ€‹โ€‹โ€‹โ€‹    #         "id": {"title": "Id", "type": "integer"},
    โ€‹โ€‹โ€‹โ€‹    #         "name": {"default": "John Doe", "title": "Name", "type": "string"},
    โ€‹โ€‹โ€‹โ€‹    #         "signup_ts": {
    โ€‹โ€‹โ€‹โ€‹    #             "anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}],
    โ€‹โ€‹โ€‹โ€‹    #             "title": "Signup Ts",
    โ€‹โ€‹โ€‹โ€‹    #         },
    โ€‹โ€‹โ€‹โ€‹    #         "tastes": {
    โ€‹โ€‹โ€‹โ€‹    #             "additionalProperties": {"exclusiveMinimum": 0, "type": "integer"},
    โ€‹โ€‹โ€‹โ€‹    #             "title": "Tastes",
    โ€‹โ€‹โ€‹โ€‹    #             "type": "object",
    โ€‹โ€‹โ€‹โ€‹    #         },
    โ€‹โ€‹โ€‹โ€‹    #     },
    โ€‹โ€‹โ€‹โ€‹    #     "required": ["id", "signup_ts", "tastes"],
    โ€‹โ€‹โ€‹โ€‹    #     "title": "User",
    โ€‹โ€‹โ€‹โ€‹    #     "type": "object",
    โ€‹โ€‹โ€‹โ€‹    # }
    
    
    โ€‹โ€‹โ€‹โ€‹if __name__ == "__main__":
    โ€‹โ€‹โ€‹โ€‹    test_user()
    

ConfigDict ่จญๅฎš Model ่กŒ็‚บ

  • ็ฏ„ไพ‹
    โ€‹โ€‹โ€‹โ€‹class User(BaseModel):
    โ€‹โ€‹โ€‹โ€‹    id: int
    โ€‹โ€‹โ€‹โ€‹    name: str
    
    โ€‹โ€‹โ€‹โ€‹    # extra="forbid" (ไธๅ…่จฑ้กๅค–ๆฌ„ไฝๅ‡บ็พๅœจ่ผธๅ…ฅ่ณ‡ๆ–™)
    โ€‹โ€‹โ€‹โ€‹    # forzen=True (ไธๅฏ่ฎŠ็‰ฉไปถ)
    โ€‹โ€‹โ€‹โ€‹    # from_attributes=True (ๅฏ้ฉ—่ญ‰ ORM - SQLAlchemy ๅ›žๅ‚ณ็‰ฉไปถ)
    โ€‹โ€‹โ€‹โ€‹    model_config = ConfigDict(extra="forbid", frozen=True)
    
    โ€‹โ€‹โ€‹โ€‹user = User(id=1, name="Rogelio")
    

Types

ๅž‹ๅˆฅ

  • Standard Library Types
  • Pydantic Types
    • Strict~ ๅšดๆ ผๅž‹ๅˆฅ
      • ไฝฟ็”จๅŽŸ็”Ÿๅž‹ๅˆฅๆจ™่จป๏ผŒ่‹ฅ็™ผ็”Ÿๅž‹ๆ…‹ไธ็›ธ็ฌฆไฝ†็›ธๅฎน๏ผŒPydantic ๆœƒ่‡ชๅ‹•้šฑๅผ่ฝ‰ๅž‹
      • ่‹ฅๅธŒๆœ›็ฆๆญข้šฑๅผ่ฝ‰ๅž‹๏ผŒ่ซ‹ไฝฟ็”จๅšดๆ ผๅž‹ๅˆฅ
  • Network Types
    • EmailStr Email
    • SecretStr ๅฏ†็ขผ
    • โ€ฆ

TypeAdaptor ่ฝ‰ๆŽฅ้ ญ

  • ็ฏ„ไพ‹

    ็„ก้ ˆ้กฏๅผ็นผๆ‰ฟ BaseModel ไนŸ่ƒฝ้€ฒ่กŒ้ฉ—่ญ‰

    โ€‹โ€‹โ€‹โ€‹from pydantic import StrictInt, TypeAdapter
    
    โ€‹โ€‹โ€‹โ€‹adapter = TypeAdapter(list[StrictInt])
    โ€‹โ€‹โ€‹โ€‹print(adapter.json_schema())  # {'items': {'type': 'integer'}, 'type': 'array'}
    โ€‹โ€‹โ€‹โ€‹print(adapter.validate_python([1, 2, 3]))  # [1, 2, 3]
    โ€‹โ€‹โ€‹โ€‹print(adapter.validate_python([1, 2, "3"]))  # ValidationError
    
    โ€‹โ€‹โ€‹โ€‹from dataclasses import dataclass
    
    โ€‹โ€‹โ€‹โ€‹from pydantic import TypeAdapter
    
    
    โ€‹โ€‹โ€‹โ€‹@dataclass
    โ€‹โ€‹โ€‹โ€‹class User:
    โ€‹โ€‹โ€‹โ€‹    name: str
    โ€‹โ€‹โ€‹โ€‹    age: int
    
    โ€‹โ€‹โ€‹โ€‹# ไนŸๅฏไปฅ่ฝ‰ๆŽฅ dataclass
    โ€‹โ€‹โ€‹โ€‹adapter = TypeAdapter(User)
    
    โ€‹โ€‹โ€‹โ€‹data = {"name": "Alice", "age": "30"}
    โ€‹โ€‹โ€‹โ€‹print(adapter.validate_python(data))  # User(name='Alice', age=30)
    

FailFast ๅ‡บ้Œฏไบ†ๅฐฑ่ถ•็ทŠไธ‹ๅฐ

  • ็ฏ„ไพ‹

    ๆชขๆŸฅๅˆฐ็ฌฌไธ€ๅ€‹้Œฏ่ชค ("invalid") ๅฐฑๆœƒ็›ดๆŽฅๅ ฑ้Œฏ๏ผŒ่€Œไธๆœƒๅฐ‡ๆ•ดๅ€‹ list ้ƒฝๆชขๆŸฅๅฎŒ็•ข

    โ€‹โ€‹โ€‹โ€‹from typing import Annotated
    
    โ€‹โ€‹โ€‹โ€‹from pydantic import FailFast, TypeAdapter, ValidationError
    
    โ€‹โ€‹โ€‹โ€‹ta = TypeAdapter(Annotated[list[bool], FailFast()])
    โ€‹โ€‹โ€‹โ€‹try:
    โ€‹โ€‹โ€‹โ€‹    ta.validate_python([True, "invalid", False, "also invalid"])
    โ€‹โ€‹โ€‹โ€‹except ValidationError as exc:
    โ€‹โ€‹โ€‹โ€‹    print(exc)
    

Fields

  • ็ฏ„ไพ‹

    example, description ็ญ‰่ณ‡่จŠ่ขซ็”จๅœจ่ˆ‡ FastAPI ๆ•ดๅˆ็š„ Swagger UI ็š„ API ๆ–‡ๆช”ไน‹ไธญ

    โ€‹โ€‹โ€‹โ€‹from enum import IntFlag, auto
    
    โ€‹โ€‹โ€‹โ€‹from pydantic import BaseModel, EmailStr, Field, SecretStr
    
    
    โ€‹โ€‹โ€‹โ€‹class Role(IntFlag):
    โ€‹โ€‹โ€‹โ€‹    Author = auto()
    โ€‹โ€‹โ€‹โ€‹    Editor = auto()
    โ€‹โ€‹โ€‹โ€‹    Developer = auto()
    โ€‹โ€‹โ€‹โ€‹    Admin = Author | Editor | Developer
    
    
    โ€‹โ€‹โ€‹โ€‹class User(BaseModel):
    โ€‹โ€‹โ€‹โ€‹    name: str = Field(
    โ€‹โ€‹โ€‹โ€‹        examples=["Arjan"],
    โ€‹โ€‹โ€‹โ€‹        description="The full name of the user",
    โ€‹โ€‹โ€‹โ€‹    )
    โ€‹โ€‹โ€‹โ€‹    email: EmailStr = Field(
    โ€‹โ€‹โ€‹โ€‹        examples=["example@arjancodes.com"],
    โ€‹โ€‹โ€‹โ€‹        description="The email address of the user",
    โ€‹โ€‹โ€‹โ€‹    )
    โ€‹โ€‹โ€‹โ€‹    password: SecretStr = Field(
    โ€‹โ€‹โ€‹โ€‹        examples=["Password123"],
    โ€‹โ€‹โ€‹โ€‹        description="The password of the user",
    โ€‹โ€‹โ€‹โ€‹    )
    โ€‹โ€‹โ€‹โ€‹    role: Role = Field(
    โ€‹โ€‹โ€‹โ€‹        default=Role.Author,
    โ€‹โ€‹โ€‹โ€‹        description="The role of the user",
    โ€‹โ€‹โ€‹โ€‹    )
    

    ไธ€ๅ€‹ๆ›ดไนพๆทจ็š„ๅฏซๆณ•

    โ€‹โ€‹โ€‹โ€‹class UserType:
    โ€‹โ€‹โ€‹โ€‹    Name = Annotated[
    โ€‹โ€‹โ€‹โ€‹        str,
    โ€‹โ€‹โ€‹โ€‹        Field(description="ไฝฟ็”จ่€…ๅง“ๅ", example="RogelioKG"),
    โ€‹โ€‹โ€‹โ€‹    ]
    โ€‹โ€‹โ€‹โ€‹    Email = Annotated[
    โ€‹โ€‹โ€‹โ€‹        EmailStr,
    โ€‹โ€‹โ€‹โ€‹        Field(description="ไฝฟ็”จ่€…้›ปๅญ้ƒตไปถ", example="user@example.com"),
    โ€‹โ€‹โ€‹โ€‹    ]
    โ€‹โ€‹โ€‹โ€‹    Avatar = Annotated[
    โ€‹โ€‹โ€‹โ€‹        str,
    โ€‹โ€‹โ€‹โ€‹        Field(description="ไฝฟ็”จ่€…้ ญๅƒๅœ–็‰‡็ถฒๅ€๏ผŒๅฏ็‚บ็ฉบ", example="https://example.com/avatar.jpg"),
    โ€‹โ€‹โ€‹โ€‹    ]
    โ€‹โ€‹โ€‹โ€‹    Password = Annotated[
    โ€‹โ€‹โ€‹โ€‹        SecretStr,
    โ€‹โ€‹โ€‹โ€‹        Field(min_length=6, description="ไฝฟ็”จ่€…็™ปๅ…ฅๅฏ†็ขผ๏ผŒ่‡ณๅฐ‘ 6 ๅ€‹ๅญ—ๅ…ƒ", example="securePass123"),
    โ€‹โ€‹โ€‹โ€‹    ]
    โ€‹โ€‹โ€‹โ€‹    Age = Annotated[
    โ€‹โ€‹โ€‹โ€‹        int,
    โ€‹โ€‹โ€‹โ€‹        Field(gt=0, lt=100, description="ไฝฟ็”จ่€…ๅนด้ฝก๏ผŒ้œ€ไป‹ๆ–ผ 1 ๅˆฐ 99 ๆญฒ", example=25),
    โ€‹โ€‹โ€‹โ€‹    ]
    โ€‹โ€‹โ€‹โ€‹    Birthday = Annotated[
    โ€‹โ€‹โ€‹โ€‹        date,
    โ€‹โ€‹โ€‹โ€‹        Field(description="ไฝฟ็”จ่€…็”Ÿๆ—ฅ๏ผˆๆ ผๅผ YYYY-MM-DD๏ผ‰", example="1998-08-08"),
    โ€‹โ€‹โ€‹โ€‹    ]
    
    
    โ€‹โ€‹โ€‹โ€‹class UserBase(BaseModel):
    โ€‹โ€‹โ€‹โ€‹    name: UserType.Name
    โ€‹โ€‹โ€‹โ€‹    email: UserType.Email
    โ€‹โ€‹โ€‹โ€‹    avatar: UserType.Avatar | None = None
    
    โ€‹โ€‹โ€‹โ€‹    model_config = ConfigDict(from_attributes=True)
    
    
    โ€‹โ€‹โ€‹โ€‹class UserCreate(UserBase):
    โ€‹โ€‹โ€‹โ€‹    password: UserType.Password
    โ€‹โ€‹โ€‹โ€‹    age: UserType.Age
    โ€‹โ€‹โ€‹โ€‹    birthday: UserType.Birthday
    
    
    โ€‹โ€‹โ€‹โ€‹class UserRead(UserBase):
    โ€‹โ€‹โ€‹โ€‹    age: UserType.Age
    โ€‹โ€‹โ€‹โ€‹    birthday: UserType.Birthday
    
    
    โ€‹โ€‹โ€‹โ€‹class UserUpdate(BaseModel):
    โ€‹โ€‹โ€‹โ€‹    name: UserType.Name | None = None
    โ€‹โ€‹โ€‹โ€‹    email: UserType.Email | None = None
    โ€‹โ€‹โ€‹โ€‹    avatar: UserType.Avatar | None = None
    โ€‹โ€‹โ€‹โ€‹    password: UserType.Password | None = None
    โ€‹โ€‹โ€‹โ€‹    age: UserType.Age | None = None
    โ€‹โ€‹โ€‹โ€‹    birthday: UserType.Birthday | None = None
    
  • discriminated union

    ไธ€็จฎ็‰นๆฎŠ็š„ Union ๅž‹ๅˆฅ๏ผŒ็”จไพ†ๆ่ฟฐๅคšๅ€‹ๅญๅž‹ๅˆฅๅ…ฑ็”จไธ€ๅ€‹ๅฑฌๆ€ง (้€šๅธธๆ˜ฏๅญ—ไธฒ)๏ผŒไธฆๆ นๆ“š้€™ๅ€‹ๅฑฌๆ€ง็š„ๅ€ผไพ†ๅ€ๅˆ†ไธๅŒๅž‹ๅˆฅ็š„็ตๆง‹ใ€‚
    ้€™ไบ›ๅญๅž‹ๅˆฅ้ƒฝๅฑฌๆ–ผๆŸๅ€‹ๅ…ฑๅŒ็š„ Union ๅž‹ๅˆฅ๏ผŒ้€šๅธธๆญ้… switch ไฝฟ็”จ่ƒฝ่ฎ“็ทจ่ญฏๅ™จ่‡ชๅ‹•้€ฒ่กŒๅž‹ๅˆฅๆŽจๆ–ท่ˆ‡ๆชขๆŸฅใ€‚

    โ€‹โ€‹โ€‹โ€‹type Shape =
    โ€‹โ€‹โ€‹โ€‹| { kind: "circle"; radius: number }
    โ€‹โ€‹โ€‹โ€‹| { kind: "square"; sideLength: number }
    โ€‹โ€‹โ€‹โ€‹| { kind: "rectangle"; width: number; height: number };
    
    โ€‹โ€‹โ€‹โ€‹function getArea(shape: Shape): number {
    โ€‹โ€‹โ€‹โ€‹switch (shape.kind) {
    โ€‹โ€‹โ€‹โ€‹    case "circle":
    โ€‹โ€‹โ€‹โ€‹    return Math.PI * shape.radius ** 2;
    โ€‹โ€‹โ€‹โ€‹    case "square":
    โ€‹โ€‹โ€‹โ€‹    return shape.sideLength ** 2;
    โ€‹โ€‹โ€‹โ€‹    case "rectangle":
    โ€‹โ€‹โ€‹โ€‹    return shape.width * shape.height;
    โ€‹โ€‹โ€‹โ€‹    default:
    โ€‹โ€‹โ€‹โ€‹    const _exhaustiveCheck: never = shape;
    โ€‹โ€‹โ€‹โ€‹    return _exhaustiveCheck;
    โ€‹โ€‹โ€‹โ€‹}
    โ€‹โ€‹โ€‹โ€‹}
    
    โ€‹โ€‹โ€‹โ€‹from typing import Literal
    
    โ€‹โ€‹โ€‹โ€‹from pydantic import BaseModel, Field, ValidationError
    
    
    โ€‹โ€‹โ€‹โ€‹class Cat(BaseModel):
    โ€‹โ€‹โ€‹โ€‹    pet_type: Literal["cat"]
    โ€‹โ€‹โ€‹โ€‹    meows: int
    
    
    โ€‹โ€‹โ€‹โ€‹class Dog(BaseModel):
    โ€‹โ€‹โ€‹โ€‹    pet_type: Literal["dog"]
    โ€‹โ€‹โ€‹โ€‹    barks: float
    
    
    โ€‹โ€‹โ€‹โ€‹class Lizard(BaseModel):
    โ€‹โ€‹โ€‹โ€‹    pet_type: Literal["reptile", "lizard"]
    โ€‹โ€‹โ€‹โ€‹    scales: bool
    
    
    โ€‹โ€‹โ€‹โ€‹class Model(BaseModel):
    โ€‹โ€‹โ€‹โ€‹    # ้€™่ฃกๅคšๆŒ‡ๅฎšไธ€ๅ€‹ discriminator๏ผŒๅฐ Pydantic ๆ•ˆ็އๅนซๅŠฉๅพˆๅคง
    โ€‹โ€‹โ€‹โ€‹    pet: Cat | Dog | Lizard = Field(discriminator="pet_type")  
    โ€‹โ€‹โ€‹โ€‹    n: int
    
    
    โ€‹โ€‹โ€‹โ€‹print(Model(pet={"pet_type": "dog", "barks": 3.14}, n=1))
    โ€‹โ€‹โ€‹โ€‹# pet=Dog(pet_type='dog', barks=3.14) n=1
    โ€‹โ€‹โ€‹โ€‹try:
    โ€‹โ€‹โ€‹โ€‹    a = Model(pet={"pet_type": "dog"}, n=1)
    โ€‹โ€‹โ€‹โ€‹except ValidationError as e:
    โ€‹โ€‹โ€‹โ€‹    print(e)
    

Validators

  • Pydantic ้ ่จญ้ฉ—่ญ‰

    • ๆœƒๅฐๅฏ่ฝ‰ๅž‹็š„่ณ‡ๆ–™๏ผŒ้€ฒ่กŒ้šฑๅผๅผทๅˆถ่ฝ‰ๅž‹
  • validator

    • field_validator ๆฌ„ไฝ้ฉ—่ญ‰ๅ™จ
    • model_validator Model ้ฉ—่ญ‰ๅ™จ
  • mode

    • mode=before๏ผšๅœจ Pydantic ้ ่จญ้ฉ—่ญ‰ไน‹ๅ‰้€ฒ่กŒ้ฉ—่ญ‰
    • mode=after๏ผšๅœจ Pydantic ้ ่จญ้ฉ—่ญ‰ไน‹ๅพŒ้€ฒ่กŒ้ฉ—่ญ‰
    • mode=wrap๏ผšๅฎŒๅ…จๆŽŒๆŽง้ฉ—่ญ‰ๆต็จ‹๏ผŒๅฏๅœจไปปๆ„ไฝ็ฝฎ้ฉ—่ญ‰ใ€ไปปๆ„ๆฑบๅฎš่ฆไธ่ฆๅŸท่กŒ Pydantic ้ ่จญ้ฉ—่ญ‰
      โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹from collections.abc import Callable
      โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹from typing import Any
      
      โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹from pydantic import BaseModel, ValidationInfo, field_validator
      
      
      โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹class User(BaseModel):
      โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹    age: int
      
      โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹    @field_validator("age", mode="wrap")
      โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹    def validate_age(
      โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹        cls,
      โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹        handler: Callable[[Any], int],  # int ๆ˜ฏ็›ฎๆจ™ๅž‹ๅˆฅ
      โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹        value: Any,
      โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹        info: ValidationInfo,
      โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹    ) -> int:
      โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹        print("raw value =", value)
      โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹        result = handler(value)  # Pydantic ้ ่จญ้ฉ—่ญ‰
      โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹        print("validated =", result)
      
      โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹        if result < 0:
      โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹            raise ValueError("Age cannot be negative!")
      
      โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹        return result
      
  • Decorator ๅฏซๆณ•

    โ€‹โ€‹โ€‹โ€‹from pydantic import BaseModel, ValidationError, field_validator
    
    
    โ€‹โ€‹โ€‹โ€‹class Model(BaseModel):
    โ€‹โ€‹โ€‹โ€‹    number: int
    
    โ€‹โ€‹โ€‹โ€‹    @field_validator('number', mode='after')  
    โ€‹โ€‹โ€‹โ€‹    @classmethod
    โ€‹โ€‹โ€‹โ€‹    def is_even_validator(cls, value: int) -> int:
    โ€‹โ€‹โ€‹โ€‹        if value % 2 == 1:
    โ€‹โ€‹โ€‹โ€‹            raise ValueError(f'{value} is not an even number')
    โ€‹โ€‹โ€‹โ€‹        return value  
    
    
    โ€‹โ€‹โ€‹โ€‹try:
    โ€‹โ€‹โ€‹โ€‹    Model(number=1)
    โ€‹โ€‹โ€‹โ€‹except ValidationError as err:
    โ€‹โ€‹โ€‹โ€‹    print(err)
    
  • Annotated ๅฏซๆณ•

    ๆญคๅฏซๆณ•็„กๆณ•็”จๆ–ผ้ฉ—่ญ‰ Model

    โ€‹โ€‹โ€‹โ€‹from typing import Annotated
    
    โ€‹โ€‹โ€‹โ€‹from pydantic import AfterValidator, BaseModel, ValidationError
    
    
    โ€‹โ€‹โ€‹โ€‹def is_even_validator(number: int) -> int:
    โ€‹โ€‹โ€‹โ€‹    if number % 2 == 1:
    โ€‹โ€‹โ€‹โ€‹        raise ValueError(f"{number} is not an even number")
    โ€‹โ€‹โ€‹โ€‹    return number
    
    
    โ€‹โ€‹โ€‹โ€‹class Model(BaseModel):
    โ€‹โ€‹โ€‹โ€‹    number: Annotated[int, AfterValidator(is_even_validator)]
    
    
    โ€‹โ€‹โ€‹โ€‹try:
    โ€‹โ€‹โ€‹โ€‹    Model(number=1)
    โ€‹โ€‹โ€‹โ€‹except ValidationError as err:
    โ€‹โ€‹โ€‹โ€‹    print(err)
    

Serializer

  • validator
    • field_serializer
    • model_serializer
  • mode
    • mode=before
    • mode=after
    • mode=wrap
  • ็ฏ„ไพ‹ (ArjanCodes ๆไพ›)
    โ€‹โ€‹โ€‹โ€‹import enum
    โ€‹โ€‹โ€‹โ€‹import hashlib
    โ€‹โ€‹โ€‹โ€‹import re
    โ€‹โ€‹โ€‹โ€‹from collections.abc import Callable
    โ€‹โ€‹โ€‹โ€‹from typing import Any, Self
    
    โ€‹โ€‹โ€‹โ€‹from pydantic import (
    โ€‹โ€‹โ€‹โ€‹    BaseModel,
    โ€‹โ€‹โ€‹โ€‹    EmailStr,
    โ€‹โ€‹โ€‹โ€‹    Field,
    โ€‹โ€‹โ€‹โ€‹    SecretStr,
    โ€‹โ€‹โ€‹โ€‹    SerializationInfo,
    โ€‹โ€‹โ€‹โ€‹    field_serializer,
    โ€‹โ€‹โ€‹โ€‹    field_validator,
    โ€‹โ€‹โ€‹โ€‹    model_serializer,
    โ€‹โ€‹โ€‹โ€‹    model_validator,
    โ€‹โ€‹โ€‹โ€‹)
    
    โ€‹โ€‹โ€‹โ€‹VALID_PASSWORD_REGEX = re.compile(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$")
    โ€‹โ€‹โ€‹โ€‹VALID_NAME_REGEX = re.compile(r"^[a-zA-Z]{2,}$")
    
    
    โ€‹โ€‹โ€‹โ€‹class Role(enum.IntFlag):
    โ€‹โ€‹โ€‹โ€‹    User = 0
    โ€‹โ€‹โ€‹โ€‹    Author = 1
    โ€‹โ€‹โ€‹โ€‹    Editor = 2
    โ€‹โ€‹โ€‹โ€‹    Admin = 4
    โ€‹โ€‹โ€‹โ€‹    SuperAdmin = 8
    
    
    โ€‹โ€‹โ€‹โ€‹class User(BaseModel):
    โ€‹โ€‹โ€‹โ€‹    name: str = Field(examples=["Example"])
    โ€‹โ€‹โ€‹โ€‹    email: EmailStr = Field(
    โ€‹โ€‹โ€‹โ€‹        examples=["user@arjancodes.com"],
    โ€‹โ€‹โ€‹โ€‹        description="The email address of the user",
    โ€‹โ€‹โ€‹โ€‹        frozen=True,  # ๆญคๆฌ„ไฝไธๅฏ่ฎŠๆ›ด
    โ€‹โ€‹โ€‹โ€‹    )
    โ€‹โ€‹โ€‹โ€‹    password: SecretStr = Field(
    โ€‹โ€‹โ€‹โ€‹        examples=["Password123"],
    โ€‹โ€‹โ€‹โ€‹        description="The password of the user",
    โ€‹โ€‹โ€‹โ€‹        exclude=True,  # ๆญคๆฌ„ไฝไธๅŠ ๅ…ฅ serialization
    โ€‹โ€‹โ€‹โ€‹    )
    โ€‹โ€‹โ€‹โ€‹    role: Role = Field(
    โ€‹โ€‹โ€‹โ€‹        description="The role of the user",
    โ€‹โ€‹โ€‹โ€‹        examples=[1, 2, 4, 8],
    โ€‹โ€‹โ€‹โ€‹        default=0,
    โ€‹โ€‹โ€‹โ€‹        validate_default=True,  # ๅณไพฟๆ˜ฏไฝฟ็”จ้ ่จญๅ€ผ๏ผŒไนŸๆœƒ่ท‘ไธ€้้ฉ—่ญ‰ๆต็จ‹
    โ€‹โ€‹โ€‹โ€‹    )
    
    โ€‹โ€‹โ€‹โ€‹    @field_validator("name")
    โ€‹โ€‹โ€‹โ€‹    def validate_name(cls, v: str) -> str:
    โ€‹โ€‹โ€‹โ€‹        if not VALID_NAME_REGEX.match(v):
    โ€‹โ€‹โ€‹โ€‹            raise ValueError(
    โ€‹โ€‹โ€‹โ€‹                "Name is invalid, must contain only letters and be at least 2 characters long"
    โ€‹โ€‹โ€‹โ€‹            )
    โ€‹โ€‹โ€‹โ€‹        return v
    
    โ€‹โ€‹โ€‹โ€‹    @field_validator("role", mode="before")
    โ€‹โ€‹โ€‹โ€‹    @classmethod
    โ€‹โ€‹โ€‹โ€‹    def validate_role(cls, v: int | str | Role) -> Role:
    โ€‹โ€‹โ€‹โ€‹        op = {int: lambda x: Role(x), str: lambda x: Role[x], Role: lambda x: x}
    โ€‹โ€‹โ€‹โ€‹        try:
    โ€‹โ€‹โ€‹โ€‹            return op[type(v)](v)
    โ€‹โ€‹โ€‹โ€‹        except (KeyError, ValueError) as exc:
    โ€‹โ€‹โ€‹โ€‹            roles = ", ".join([x.name for x in Role])
    โ€‹โ€‹โ€‹โ€‹            raise ValueError(f"Role is invalid, please use one of the following: {roles}") from exc
    
    โ€‹โ€‹โ€‹โ€‹    @model_validator(mode="before")
    โ€‹โ€‹โ€‹โ€‹    @classmethod
    โ€‹โ€‹โ€‹โ€‹    def validate_user_pre(cls, v: dict[str, Any]) -> dict[str, Any]:
    โ€‹โ€‹โ€‹โ€‹        if "name" not in v or "password" not in v:
    โ€‹โ€‹โ€‹โ€‹            raise ValueError("Name and password are required")
    โ€‹โ€‹โ€‹โ€‹        if v["name"].casefold() in v["password"].casefold():
    โ€‹โ€‹โ€‹โ€‹            raise ValueError("Password cannot contain name")
    โ€‹โ€‹โ€‹โ€‹        if not VALID_PASSWORD_REGEX.match(v["password"]):
    โ€‹โ€‹โ€‹โ€‹            raise ValueError(
    โ€‹โ€‹โ€‹โ€‹                "Password is invalid, must contain 8 characters, 1 uppercase, 1 lowercase, 1 number"
    โ€‹โ€‹โ€‹โ€‹            )
    โ€‹โ€‹โ€‹โ€‹        v["password"] = hashlib.sha256(v["password"].encode()).hexdigest()
    โ€‹โ€‹โ€‹โ€‹        return v
    
    โ€‹โ€‹โ€‹โ€‹    @field_serializer("role", when_used="json")
    โ€‹โ€‹โ€‹โ€‹    @classmethod
    โ€‹โ€‹โ€‹โ€‹    def serialize_role(cls, v: Role) -> str:
    โ€‹โ€‹โ€‹โ€‹        return v.name
    
    โ€‹โ€‹โ€‹โ€‹    @model_validator(mode="after")
    โ€‹โ€‹โ€‹โ€‹    def validate_user_post(self, v: Any) -> Self:
    โ€‹โ€‹โ€‹โ€‹        if self.role == Role.Admin and self.name != "Arjan":
    โ€‹โ€‹โ€‹โ€‹            raise ValueError("Only Arjan can be an admin")
    โ€‹โ€‹โ€‹โ€‹        return self
    
    โ€‹โ€‹โ€‹โ€‹    @model_serializer(mode="wrap", when_used="json")
    โ€‹โ€‹โ€‹โ€‹    def serialize_user(
    โ€‹โ€‹โ€‹โ€‹        self,
    โ€‹โ€‹โ€‹โ€‹        serializer: Callable[[BaseModel], dict[str, Any]],  # Pydantic ้ ่จญๅบๅˆ—ๅŒ–
    โ€‹โ€‹โ€‹โ€‹        info: SerializationInfo,
    โ€‹โ€‹โ€‹โ€‹    ) -> dict[str, Any]:
    โ€‹โ€‹โ€‹โ€‹        if not info.include and not info.exclude:
    โ€‹โ€‹โ€‹โ€‹            return {"name": self.name, "role": self.role.name}
    โ€‹โ€‹โ€‹โ€‹        return serializer(self)
    
    
    โ€‹โ€‹โ€‹โ€‹def main() -> None:
    โ€‹โ€‹โ€‹โ€‹    data = {
    โ€‹โ€‹โ€‹โ€‹        "name": "Arjan",
    โ€‹โ€‹โ€‹โ€‹        "email": "example@arjancodes.com",
    โ€‹โ€‹โ€‹โ€‹        "password": "Password123",
    โ€‹โ€‹โ€‹โ€‹        "role": "Admin",
    โ€‹โ€‹โ€‹โ€‹    }
    โ€‹โ€‹โ€‹โ€‹    user = User.model_validate(data)
    โ€‹โ€‹โ€‹โ€‹    if user:
    โ€‹โ€‹โ€‹โ€‹        print(
    โ€‹โ€‹โ€‹โ€‹            "The serializer that returns a dict:",
    โ€‹โ€‹โ€‹โ€‹            user.model_dump(),
    โ€‹โ€‹โ€‹โ€‹            sep="\n",
    โ€‹โ€‹โ€‹โ€‹            end="\n\n",
    โ€‹โ€‹โ€‹โ€‹        )
    โ€‹โ€‹โ€‹โ€‹        # The serializer that returns a dict:
    โ€‹โ€‹โ€‹โ€‹        # {
    โ€‹โ€‹โ€‹โ€‹        #     "name": "Arjan",
    โ€‹โ€‹โ€‹โ€‹        #     "email": "example@arjancodes.com",
    โ€‹โ€‹โ€‹โ€‹        #     "role": <Role.Admin: 4>,
    โ€‹โ€‹โ€‹โ€‹        # }
    
    โ€‹โ€‹โ€‹โ€‹        print(
    โ€‹โ€‹โ€‹โ€‹            "The serializer that returns a JSON string:",
    โ€‹โ€‹โ€‹โ€‹            user.model_dump(mode="json"),
    โ€‹โ€‹โ€‹โ€‹            sep="\n",
    โ€‹โ€‹โ€‹โ€‹            end="\n\n",
    โ€‹โ€‹โ€‹โ€‹        )
    โ€‹โ€‹โ€‹โ€‹        # The serializer that returns a JSON string:
    โ€‹โ€‹โ€‹โ€‹        # {
    โ€‹โ€‹โ€‹โ€‹        #     "name": "Arjan",
    โ€‹โ€‹โ€‹โ€‹        #     "role": "Admin",
    โ€‹โ€‹โ€‹โ€‹        # }
    
    โ€‹โ€‹โ€‹โ€‹        print(
    โ€‹โ€‹โ€‹โ€‹            "The serializer that returns a json string, excluding the role:",
    โ€‹โ€‹โ€‹โ€‹            user.model_dump(exclude=["role"], mode="json"),
    โ€‹โ€‹โ€‹โ€‹            sep="\n",
    โ€‹โ€‹โ€‹โ€‹            end="\n\n",
    โ€‹โ€‹โ€‹โ€‹        )
    โ€‹โ€‹โ€‹โ€‹        # The serializer that returns a json string, excluding the role:
    โ€‹โ€‹โ€‹โ€‹        # {
    โ€‹โ€‹โ€‹โ€‹        #     "name": "Arjan",
    โ€‹โ€‹โ€‹โ€‹        #     "email": "example@arjancodes.com",
    โ€‹โ€‹โ€‹โ€‹        # }
    
    โ€‹โ€‹โ€‹โ€‹        print("The serializer that encodes all values to a dict:", dict(user), sep="\n")
    โ€‹โ€‹โ€‹โ€‹        # The serializer that encodes all values to a dict:
    โ€‹โ€‹โ€‹โ€‹        # {
    โ€‹โ€‹โ€‹โ€‹        #     "name": "Arjan",
    โ€‹โ€‹โ€‹โ€‹        #     "email": "example@arjancodes.com",
    โ€‹โ€‹โ€‹โ€‹        #     "password": SecretStr("**********"),
    โ€‹โ€‹โ€‹โ€‹        #     "role": <Role.Admin: 4>,
    โ€‹โ€‹โ€‹โ€‹        # }
    
    
    โ€‹โ€‹โ€‹โ€‹if __name__ == "__main__":
    โ€‹โ€‹โ€‹โ€‹    main()
    

Performace

Pydantic - ้กงๆ…ฎๆ€ง่ƒฝ็š„ best practices๏ผŒไปฅไธ‹ๅƒ…ๅš็ฐก่ฟฐใ€‚

  • ๅคš็”จ FailFast
  • ๅคš็”จ TypedDict + TypeAdapter๏ผŒๅฐ‘็”จ nested BaseModel
  • TypeAdapter ่ƒฝ้‡่ค‡ๅˆฉ็”จๆœ€ๅฅฝ (ๆ‰€ไปฅๅˆฅๆŠŠๅฎƒๅฏซๅœจๅ‡ฝๅผ่ฃก)
  • discriminated union ่จ˜ๅพ—ๆŒ‡ๅฎš discriminator