Pydantic v2: Rust로 재작성

Pydantic v2(2023년 6월)는 완전히 재작성되었습니다: 검증 코어 이제 Rust로 구현되었습니다(pydantic-core), 5-50x로 만듭니다. 대부분의 사용 사례에서 v1보다 빠릅니다. FastAPI 0.100+ 사용 기본적으로 Pydantic v2.

무엇을 배울 것인가

  • Pydantic v1과 v2의 차이점: API 변경, 마이그레이션 대상
  • model_validate 및 model_dump: 새로운 직렬화 API
  • Field(): 제약 조건, 별칭, 기본 팩토리
  • field_validator 및 model_validator: 모드가 있는 유효성 검사기
  • TypeAdapter: BaseModel이 아닌 유형에 대한 유효성 검사
  • ConfigDict: 내부 Config 클래스가 없는 모델 구성
  • model_rebuild: 전방 참조 및 원형 모델

설치 및 전제조건

# Pydantic v2 (installato automaticamente con FastAPI recente)
pip install pydantic[email]  # Include EmailStr e validatori email
pip install pydantic-settings  # Per configurazione app

# Verifica versione
python -c "import pydantic; print(pydantic.VERSION)"
# 2.x.x

BaseModel: 기본 구조

Pydantic 모델은 다음을 상속하는 Python 클래스입니다. BaseModel. 각 필드는 유형 주석이 있는 클래스 속성입니다. 피단틱 자동으로 메소드를 생성합니다 __init__, 검증 및 직렬화.

# Modello base con Pydantic v2
from pydantic import BaseModel, Field, EmailStr
from typing import Optional
from datetime import datetime
from enum import Enum

class UserStatus(str, Enum):
    active = "active"
    inactive = "inactive"
    banned = "banned"

class Address(BaseModel):
    street: str
    city: str
    country: str = "IT"  # Default value
    postal_code: Optional[str] = None

class User(BaseModel):
    # Field() fornisce metadata e vincoli
    id: int = Field(gt=0, description="User ID, must be positive")
    name: str = Field(
        min_length=2,
        max_length=100,
        description="Full name",
        examples=["Mario Rossi"],
    )
    email: EmailStr
    status: UserStatus = UserStatus.active
    address: Optional[Address] = None  # Modello annidato (nested model)

    # Field con default_factory: valore di default calcolato al momento della creazione
    tags: list[str] = Field(default_factory=list)
    created_at: datetime = Field(default_factory=datetime.now)

    # Alias: nome diverso nella serializzazione JSON
    internal_id: str = Field(alias="internalId", default="")

# Creazione con keyword arguments
user = User(
    id=1,
    name="Mario Rossi",
    email="mario@example.com",
    address=Address(street="Via Roma 1", city="Milano"),
    tags=["developer", "python"],
)

# v2: model_dump() sostituisce .dict()
user_dict = user.model_dump()
print(user_dict)
# {"id": 1, "name": "Mario Rossi", "email": "mario@example.com", ...}

# Con exclude e include
minimal = user.model_dump(include={"id", "name", "email"})
without_dates = user.model_dump(exclude={"created_at"})

# JSON string
user_json = user.model_dump_json()

model_validate: 외부 데이터에서 모델 구축

model_validate() 모델을 구축하는 v2 방식입니다. 사전 또는 임의의 객체. 직접 생성자 e를 대체합니다. .parse_obj() v1의

# model_validate: parsing da diverse sorgenti
import json

# Da dizionario Python
data = {
    "id": 1,
    "name": "Mario Rossi",
    "email": "mario@example.com",
}
user = User.model_validate(data)

# Da JSON string (convenienza)
json_data = '{"id": 2, "name": "Luigi Bianchi", "email": "luigi@example.com"}'
user2 = User.model_validate_json(json_data)

# Con alias: se il JSON usa camelCase ma il modello usa snake_case
raw_data = {"internalId": "abc-123", "id": 3, "name": "Test User", "email": "test@example.com"}
user3 = User.model_validate(raw_data)
print(user3.internal_id)  # "abc-123" - letto dall'alias

# Validazione di dati da ORM (SQLAlchemy objects)
# Pydantic v2 puo leggere attributi da oggetti non-dict
class SQLAlchemyUser:
    def __init__(self):
        self.id = 1
        self.name = "ORM User"
        self.email = "orm@example.com"

orm_obj = SQLAlchemyUser()
user4 = User.model_validate(orm_obj, from_attributes=True)
# from_attributes=True: legge attributi invece che chiavi dict

유효성 검사기: field_validator 및 model_validator

Pydantic v2는 유효성 검사기를 완전히 재설계했습니다. 두 개의 주요 데코레이터 나는 @field_validator (개별 필드의 경우) e @model_validator (교차 필드 검증용)

# Validators in Pydantic v2
from pydantic import BaseModel, field_validator, model_validator, ValidationError
from typing import Any

class PaymentOrder(BaseModel):
    amount: float
    currency: str
    discount_percent: float = 0.0
    final_amount: float = 0.0

    # field_validator: valida un singolo campo
    # mode="before": eseguito PRIMA della conversione di tipo
    # mode="after": eseguito DOPO (default in v2)
    @field_validator("currency", mode="before")
    @classmethod
    def normalize_currency(cls, v: Any) -> str:
        if isinstance(v, str):
            return v.upper().strip()  # "eur" -> "EUR"
        return v

    @field_validator("currency")
    @classmethod
    def validate_currency(cls, v: str) -> str:
        supported = {"EUR", "USD", "GBP", "JPY"}
        if v not in supported:
            raise ValueError(f"Currency {v} not supported. Use: {supported}")
        return v

    @field_validator("amount", "discount_percent")
    @classmethod
    def must_be_positive(cls, v: float) -> float:
        if v < 0:
            raise ValueError("Must be non-negative")
        return v

    # model_validator: accesso a tutti i campi dopo la validazione
    # mode="after": riceve il modello gia validato
    @model_validator(mode="after")
    def compute_final_amount(self) -> "PaymentOrder":
        discount = self.amount * (self.discount_percent / 100)
        self.final_amount = round(self.amount - discount, 2)
        return self

    # model_validator mode="before": riceve il dict grezzo
    @model_validator(mode="before")
    @classmethod
    def check_required_fields(cls, data: Any) -> Any:
        if isinstance(data, dict):
            if "amount" not in data:
                raise ValueError("amount is required")
        return data

# Test
try:
    order = PaymentOrder(amount=100.0, currency="eur", discount_percent=10.0)
    print(order.currency)      # "EUR" (normalizzato)
    print(order.final_amount)  # 90.0 (calcolato)
except ValidationError as e:
    print(e.errors())  # Lista strutturata degli errori

TypeAdapter: BaseModel이 아닌 유형에 대한 유효성 검사

TypeAdapter v2의 가장 유용한 새로운 기능 중 하나입니다. 생성하지 않고도 모든 Python 유형에 대해 Pydantic 유효성 검사를 사용할 수 있습니다. 에 BaseModel 헌신적인.

# TypeAdapter: validazione di tipi primitivi e complessi
from pydantic import TypeAdapter
from typing import List, Dict, Union

# Validazione di una lista di int
int_list_adapter = TypeAdapter(List[int])
validated = int_list_adapter.validate_python([1, "2", 3.0])
print(validated)  # [1, 2, 3] - coercizione automatica

# Validazione di un tipo Union
NumberOrString = Union[int, str]
ns_adapter = TypeAdapter(NumberOrString)
print(ns_adapter.validate_python(42))    # 42
print(ns_adapter.validate_python("hello"))  # "hello"

# Validazione di dict complessi
UserDict = Dict[str, Union[int, str, List[str]]]
dict_adapter = TypeAdapter(UserDict)
result = dict_adapter.validate_python({
    "name": "Mario",
    "age": "30",  # Stringa che viene coerta a int? No: rimane str perche Union[int, str]
    "tags": ["dev", "python"],
})

# Uso pratico: validare config da variabili d'ambiente
from typing import Annotated
from pydantic import Field as PydanticField

PositiveInt = Annotated[int, PydanticField(gt=0)]
port_adapter = TypeAdapter(PositiveInt)

try:
    port = port_adapter.validate_python(int("8080"))  # 8080
    port = port_adapter.validate_python(-1)           # ValidationError!
except Exception as e:
    print(e)

# Serializzazione con TypeAdapter
data = [1, 2, 3]
json_str = int_list_adapter.dump_json(data)  # b'[1,2,3]'

ConfigDict: 모델 구성

v2에서는 템플릿 구성이 다음을 사용합니다. model_config = ConfigDict(...) 수업 대신 Config v1의 내부.

# ConfigDict: tutte le opzioni principali
from pydantic import BaseModel, ConfigDict

class APIResponse(BaseModel):
    model_config = ConfigDict(
        # Permette la lettura da attributi ORM (SQLAlchemy, Django ORM)
        from_attributes=True,

        # Usa alias invece del nome Python nella serializzazione JSON
        populate_by_name=True,  # Permette anche il nome Python (non solo l'alias)

        # Serializzazione: converti enum al loro valore
        use_enum_values=True,

        # Validation: accetta campi extra senza errore (li ignora)
        extra="ignore",  # "allow", "ignore", "forbid"

        # JSON schema: titolo personalizzato
        title="API Response Model",

        # Validazione al momento dell'assegnazione (non solo alla creazione)
        validate_assignment=True,

        # Frozen: rende il modello immutabile dopo la creazione
        frozen=False,  # True = immutabile, genera __hash__

        # Stripping whitespace dagli str automaticamente
        str_strip_whitespace=True,

        # Serializzazione: esclude None per default
        # (utile per API che non vogliono campi null nel JSON)
        # Non disponibile come ConfigDict, usa model_dump(exclude_none=True)
    )

    user_id: int
    user_name: str  # str_strip_whitespace rimuove spazi iniziali/finali

# Esempio: modello con from_attributes per ORM
class OrmUser(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    name: str
    email: str

# Compatibile con oggetti SQLAlchemy
class FakeOrmObject:
    id = 1
    name = "  Mario Rossi  "  # Con spazi
    email = "mario@example.com"

orm_obj = FakeOrmObject()
user = OrmUser.model_validate(orm_obj)
print(repr(user.name))  # "Mario Rossi" (spazi rimossi da str_strip_whitespace)

Pydantic v1에서 v2로 마이그레이션

Pydantic v1이 포함된 기존 코드가 있는 경우 가장 일반적인 변경 사항은 다음과 같습니다.

# PYDANTIC v1 -> v2: Cheat Sheet

# 1. .dict() -> .model_dump()
user.dict()          # v1
user.model_dump()    # v2

# 2. .json() -> .model_dump_json()
user.json()          # v1
user.model_dump_json()  # v2

# 3. .parse_obj() -> .model_validate()
User.parse_obj(data)    # v1
User.model_validate(data)  # v2

# 4. .parse_raw() -> .model_validate_json()
User.parse_raw(json_str)       # v1
User.model_validate_json(json_str)  # v2

# 5. class Config -> model_config = ConfigDict()
# v1:
class User(BaseModel):
    class Config:
        orm_mode = True

# v2:
class User(BaseModel):
    model_config = ConfigDict(from_attributes=True)  # orm_mode -> from_attributes

# 6. Validators: @validator -> @field_validator
# v1:
from pydantic import validator
class User(BaseModel):
    @validator("name")
    def name_must_not_be_empty(cls, v):
        return v.strip()

# v2:
from pydantic import field_validator
class User(BaseModel):
    @field_validator("name", mode="after")
    @classmethod
    def name_must_not_be_empty(cls, v: str) -> str:
        return v.strip()

# 7. @root_validator -> @model_validator
# v1:
from pydantic import root_validator
class Model(BaseModel):
    @root_validator
    def check_fields(cls, values):
        return values

# v2:
from pydantic import model_validator
class Model(BaseModel):
    @model_validator(mode="after")
    def check_fields(self) -> "Model":
        return self

결론

Pydantic v2는 Python 데이터 검증을 훨씬 더 빠르게 만들었습니다. 그리고 더 표현력이 좋아졌습니다. Rust 코어는 또한 뛰어난 성능을 보장합니다. 집중적으로 검증하는 동시에 TypeAdapter 사용 사례를 해결합니다. 전용 모델을 만들지 않고도 유형을 검증할 수 있습니다. FastAPI에서는 모든 엔드포인트가 이점을 얻습니다. 이러한 최적화를 자동으로 수행합니다.

FastAPI 시리즈의 향후 기사

  • 제4조: FastAPI의 종속성 주입: 깨끗하고 테스트 가능한 코드를 위한 종속성()
  • 제5조: SQLAlchemy 2.0, AsyncSession 및 Alembic을 사용한 비동기 데이터베이스
  • 제6조: 백그라운드 작업: BackgroundTasks에서 Celery 및 ARQ까지