Pydantic v2: Rust'ta Yeniden Yazma

Pydantic v2 (Haziran 2023) tamamen yeniden yazılmıştır: doğrulama çekirdeği artık Rust'ta uygulanıyor (pydantic-core), 5-50x yapıyor çoğu kullanım durumunda v1'den daha hızlıdır. FastAPI 0.100+ kullanım Pydantic v2 yerel olarak.

Ne Öğreneceksiniz

  • Pydantic v1 ve v2 arasındaki farklar: API'ler değişti, nelerin taşınması gerekiyor
  • model_validate ve model_dump: yeni serileştirme API'si
  • Field(): kısıtlama, takma ad, varsayılan fabrikalar
  • field_validator ve model_validator: modlu doğrulayıcılar
  • TypeAdapter: BaseModel olmayan türler için doğrulama
  • ConfigDict: Dahili Config sınıfı olmayan model konfigürasyonu
  • model_rebuild: ileri referanslar ve dairesel modeller

Kurulum ve Önkoşullar

# 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: Temel Yapı

Pydantic modelleri, Python sınıflarından miras alan Python sınıflarıdır. BaseModel. Her alan, tür açıklamasına sahip bir sınıf niteliğidir. Pdantik yöntemi otomatik olarak oluşturur __init__, doğrulama ve serileştirme.

# 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: Dış Verilerden Model Oluşturma

model_validate() bir model oluşturmanın v2 yoludur bir sözlük veya rastgele bir nesne. Doğrudan yapıcı e'nin yerini alır .parse_obj() v1'in.

# 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

Doğrulayıcılar: field_validator ve model_validator

Pydantic v2, doğrulayıcıları tamamen yeniden tasarladı. İki ana dekoratör ben @field_validator (bireysel alanlar için) e @model_validator (alanlar arası doğrulamalar için).

# 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 Olmayan Türler için Doğrulama

TypeAdapter v2'nin en kullanışlı yeni özelliklerinden biridir: oluşturmadan herhangi bir Python türünde Pydantic doğrulamasını kullanmanıza olanak tanır bir BaseModel özel.

# 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: Model Yapılandırması

V2'de şablon yapılandırması şunları kullanır: model_config = ConfigDict(...) sınıf yerine Config v1'in içi.

# 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'den v2'ye geçiş

Pydantic v1'e sahip mevcut kodunuz varsa en yaygın değişiklikler şunlardır:

# 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

Sonuçlar

Pydantic v2, Python veri doğrulamasını önemli ölçüde daha hızlı hale getirdi ve daha etkileyici. Rust çekirdeği aynı zamanda mükemmel performansı da garanti eder. yoğun doğrulama, TypeAdapter kullanım durumunu çözer Özel modeller oluşturmadan türleri doğrulayın. FastAPI'de her uç nokta fayda sağlar bu optimizasyonların otomatik olarak yapılması.

FastAPI Serisinde Gelecek Makaleler

  • Madde 4: FastAPI'de Bağımlılık Enjeksiyonu: Temiz ve Test Edilebilir Kod için Depends()
  • Madde 5: SQLAlchemy 2.0, AsyncSession ve Alembic ile Async Veritabanı
  • Madde 6: Arka Plan Görevleri: Arka Plan Görevlerinden Kereviz ve ARQ'ya