Pydantic v2: Zaawansowana walidacja, BaseModel, TypeAdapter i ConfigDict
Poznaj nowości w Pydantic v2 (rdzeń Rust, 5–50x szybszy): Nowe API model_validate/model_dump, TypeAdapter do walidacji innej niż modelowa, walidatory z mode='przed'/'po' i migracja z wersji 1.
Pydantic v2: Przepisanie w rdzy
Pydantic v2 (czerwiec 2023 r.) to kompletna przeróbka: rdzeń walidacyjny
jest teraz zaimplementowany w Rust (pydantic-core), co daje 5-50x
szybszy niż v1 w większości przypadków użycia. Używa FastAPI 0.100+
Pydantic v2 natywnie.
Czego się nauczysz
- Różnice pomiędzy Pydantic v1 i v2: Zmieniono API, co migrować
- model_validate i model_dump: nowy interfejs API serializacji
- Pole(): ograniczenie, alias, domyślne fabryki
- Field_validator i model_validator: walidatory z trybem
- TypeAdapter: weryfikacja typów innych niż BaseModel
- ConfigDict: Konfiguracja modelu bez wewnętrznej klasy Config
- model_rebuild: odniesienia do przodu i modele cykliczne
Instalacja i wymagania wstępne
# 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: Struktura podstawowa
Modele Pydantic to klasy Pythona, które dziedziczą z BaseModel.
Każde pole jest atrybutem klasy z adnotacją typu. Pydantyczny
automatycznie generuje metodę __init__, walidacja i
serializacja.
# 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: Budowanie modeli z danych zewnętrznych
model_validate() jest to sposób na zbudowanie modelu w wersji 2
słownik lub dowolny obiekt. Zastępuje bezpośredniego konstruktora np
.parse_obj() z 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
Walidatory: Field_validator i model_validator
Pydantic v2 całkowicie przeprojektował walidatory. Dwóch głównych dekoratorów
jestem @field_validator (dla poszczególnych pól) e @model_validator
(do walidacji międzypolowych).
# 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: weryfikacja typów innych niż BaseModel
TypeAdapter to jedna z najbardziej przydatnych nowych funkcji wersji 2:
pozwala na użycie walidacji Pydantic na dowolnym typie Pythona bez tworzenia
a BaseModel poświęcony.
# 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: Konfiguracja modelu
W wersji 2 konfiguracja szablonu używa model_config = ConfigDict(...)
zamiast zajęć Config wewnętrzne 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)
Migracja z Pydantic v1 do v2
Jeśli masz już kod w Pydantic v1, oto najczęstsze zmiany:
# 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
Wnioski
Pydantic v2 znacznie przyspieszył weryfikację danych Pythona
i bardziej wyraziste. Rdzeń Rust gwarantuje również doskonałą wydajność
intensywna walidacja, podczas gdy TypeAdapter rozwiązuje przypadek użycia
sprawdzaj typy bez tworzenia dedykowanych modeli. W FastAPI każdy punkt końcowy zyskuje
automatycznie tych optymalizacji.
Nadchodzące artykuły z serii FastAPI
- Artykuł 4: Wstrzykiwanie zależności w FastAPI: Zależności () dla czystego i testowalnego kodu
- Artykuł 5: Async Database z SQLAlchemy 2.0, AsyncSession i Alembic
- Artykuł 6: Zadania w tle: od zadań w tle po seler i ARQ







