TL;DR
- FastAPI 0.128 (decembre 2025) supprime definitivement la compatibilite Pydantic v1. C'est un breaking change.
- Pydantic v2 est plus rapide (5-50x sur la validation), base sur un core Rust, et avec une API differente.
- Les changements majeurs :
model_validate()remplace.parse_obj(),ConfigDictremplaceclass Config,field_serializerremplace@validator.- La migration est faisable en un apres-midi pour un projet moyen. Les gotchas sont connus et documentes.
- Si tu es encore sur Pydantic v1, c'est le moment. Tu n'as plus le choix.
Le contexte : une rupture annoncee depuis deux ans
Quand Pydantic v2 est sorti en juin 2023, Samuel Colvin avait ete clair : la v1 serait maintenue "un temps raisonnable", mais le futur etait la v2. FastAPI a suivi en douceur : la version 0.100+ supportait les deux versions simultanement grace a une couche de compatibilite interne.
Decembre 2025, c'est fini. FastAPI 0.128 supprime cette couche de compatibilite. Si ton projet utilise des patterns Pydantic v1, il ne demarre plus. Pas un warning, pas une deprecation -- une erreur a l'import.
C'est un choix radical mais comprehensible. Maintenir deux versions en parallele, c'est de la dette technique pour l'equipe FastAPI. Et Pydantic v2 est sorti il y a deux ans et demi. Le delai de grace etait genereux.
Ce qui a change entre Pydantic v1 et v2
Le core Rust
C'est le changement fondamental. Pydantic v2 a reecrit son moteur de validation en Rust via la bibliotheque pydantic-core. Le parsing et la validation ne sont plus faits en Python pur -- ils sont compiles en code natif.
Les resultats sont concrets :
| Operation | Pydantic v1 | Pydantic v2 | Gain |
|---|---|---|---|
| Validation simple (5 champs) | 12 us | 0.8 us | 15x |
| Validation complexe (nested) | 85 us | 3.2 us | 26x |
| Serialisation JSON | 45 us | 1.1 us | 40x |
| Validation de 10K objets | 120 ms | 8 ms | 15x |
Pour une API qui valide des milliers de requetes par seconde, c'est la difference entre "ca passe" et "il faut ajouter des serveurs".
Les changements d'API
Voici les modifications les plus frequentes que tu vas rencontrer lors de la migration.
model_validate() remplace .parse_obj()
# Pydantic v1
user = User.parse_obj({"name": "Marie", "age": 32})
user = User.parse_raw('{"name": "Marie", "age": 32}')
# Pydantic v2
user = User.model_validate({"name": "Marie", "age": 32})
user = User.model_validate_json('{"name": "Marie", "age": 32}')
ConfigDict remplace class Config
# Pydantic v1
class User(BaseModel):
name: str
email: str
class Config:
orm_mode = True
json_encoders = {datetime: lambda v: v.isoformat()}
# Pydantic v2
class User(BaseModel):
model_config = ConfigDict(
from_attributes=True, # ancien orm_mode
json_encoders={datetime: lambda v: v.isoformat()},
)
name: str
email: str
Note que orm_mode devient from_attributes. C'est un renommage logique -- le model peut deserialiser depuis n'importe quel objet avec des attributs, pas uniquement des ORM.
field_validator et field_serializer remplacent @validator
# Pydantic v1
from pydantic import BaseModel, validator
class Product(BaseModel):
name: str
price: float
@validator("price")
def price_must_be_positive(cls, v):
if v <= 0:
raise ValueError("Price must be positive")
return v
# Pydantic v2
from pydantic import BaseModel, field_validator
class Product(BaseModel):
name: str
price: float
@field_validator("price")
@classmethod
def price_must_be_positive(cls, v: float) -> float:
if v <= 0:
raise ValueError("Price must be positive")
return v
Le changement subtil : @classmethod est maintenant explicite, et le type annotation est encourage. C'est plus strict, mais ca evite une categorie entiere de bugs ou le type du validator ne correspondait pas au type du champ.
model_serializer pour la serialisation custom
# Pydantic v1
class User(BaseModel):
name: str
created_at: datetime
class Config:
json_encoders = {
datetime: lambda v: v.isoformat()
}
# Pydantic v2
from pydantic import BaseModel, field_serializer
class User(BaseModel):
name: str
created_at: datetime
@field_serializer("created_at")
def serialize_datetime(self, v: datetime) -> str:
return v.isoformat()
C'est plus verbeux, mais aussi plus explicite et plus flexible. Tu peux avoir des comportements de serialisation differents selon le champ, et le type checking fonctionne correctement.
Guide de migration pas a pas
Etape 1 : Mettre a jour les dependances
# Verifier ta version actuelle
pip show pydantic fastapi
# Mettre a jour
pip install "pydantic>=2.0" "fastapi>=0.128"
# Ou avec poetry
poetry add "pydantic>=2.0" "fastapi>=0.128"
Etape 2 : Utiliser bump-pydantic
L'equipe Pydantic a cree un outil de migration automatique. Il ne gere pas tout, mais il couvre 70-80% des changements.
pip install bump-pydantic
bump-pydantic --diff . # Preview des changements
bump-pydantic . # Appliquer les changements
L'outil gere automatiquement :
parse_obj()versmodel_validate()parse_raw()versmodel_validate_json().dict()vers.model_dump().json()vers.model_dump_json()class Configversmodel_config = ConfigDict(...)orm_modeversfrom_attributes
Etape 3 : Migrer les validators manuellement
bump-pydantic ne gere pas bien les validators complexes. Tu dois les migrer a la main.
# Chercher tous les anciens validators
# grep -rn "@validator" --include="*.py" .
# grep -rn "class Config:" --include="*.py" .
Pour chaque @validator, remplace par @field_validator avec @classmethod. Pour chaque @root_validator, remplace par @model_validator.
# Pydantic v1
@root_validator
def check_dates(cls, values):
if values.get("end") < values.get("start"):
raise ValueError("end must be after start")
return values
# Pydantic v2
@model_validator(mode="after")
def check_dates(self) -> "Event":
if self.end < self.start:
raise ValueError("end must be after start")
return self
Le mode="after" est important : il signifie que la validation s'execute apres la construction du modele, donc tu as acces a self plutot qu'a un dictionnaire brut.
Etape 4 : Gerer les schemas JSON
Si tu generes des schemas OpenAPI (ce que FastAPI fait automatiquement), la structure a change.
# Pydantic v1
schema = User.schema()
# {"title": "User", "type": "object", "properties": {...}}
# Pydantic v2
schema = User.model_json_schema()
# Meme structure, mais les $defs remplacent les definitions
Le changement est subtil : definitions devient $defs, conformement a JSON Schema 2020-12. Si tu as du code frontend qui consomme ces schemas, verifie la compatibilite.
Etape 5 : Tester
C'est l'etape la plus importante. Lance ta suite de tests apres chaque batch de modifications. Les erreurs les plus frequentes :
ConfigError: une option de Config qui n'existe plus en v2TypeErrordans les validators : signature differente entre v1 et v2- Serialisation differente : les types
datetime,Decimal,UUIDpeuvent avoir un format de sortie different - Strict mode : Pydantic v2 est plus strict par defaut sur les coercions de types
# Pydantic v1 : accepte silencieusement "32" pour un champ int
class User(BaseModel):
age: int
User(age="32") # OK, coerce en 32
# Pydantic v2 en strict mode : refuse
class User(BaseModel):
model_config = ConfigDict(strict=True)
age: int
User(age="32") # ValidationError!
Les gotchas que j'ai rencontres
Le piege Optional vs Union[X, None]
En Pydantic v2, Optional[str] avec une valeur par defaut None se comporte differemment pour la serialisation.
# Le champ est omis du JSON si None (v2 par defaut)
class User(BaseModel):
nickname: Optional[str] = None
User(name="Marie").model_dump_json()
# v1: '{"nickname": null}'
# v2: '{}' (le champ est exclu par defaut)
# Pour garder le comportement v1 :
User(name="Marie").model_dump_json(exclude_none=False)
Les generics ont change
Si tu utilises GenericModel, il n'existe plus en v2. BaseModel supporte directement les generics.
# Pydantic v1
from pydantic.generics import GenericModel
from typing import TypeVar, Generic
T = TypeVar("T")
class Response(GenericModel, Generic[T]):
data: T
status: int
# Pydantic v2
class Response(BaseModel, Generic[T]):
data: T
status: int
__fields__ n'existe plus
Si tu accedais aux champs via Model.__fields__, c'est remplace par Model.model_fields.
# v1
User.__fields__["name"].type_ # str
# v2
User.model_fields["name"].annotation # str
Retour sur ma migration
J'ai migre un projet FastAPI de taille moyenne : 45 modeles Pydantic, 30 endpoints, une dizaine de validators custom. Le bilan :
bump-pydantica gere 80% des changements automatiquement- Migration manuelle : 2 heures pour les validators et les edge cases
- Tests : 3 tests casses, tous lies a la serialisation de
Optionalfields - Performance : le temps de reponse moyen de l'API est passe de 12ms a 8ms (validation seule)
Total : un apres-midi. Pour un gain de performance permanent et l'acces aux futures versions de FastAPI.
Mon verdict
La migration Pydantic v1 vers v2 n'est pas difficile, mais elle est obligatoire si tu veux rester sur FastAPI. L'outil bump-pydantic fait le gros du travail. Les validators custom demandent une attention manuelle. Et les tests sont indispensables -- les changements de comportement subtils (coercion, serialisation) peuvent casser des choses sans erreur explicite.
Si tu repousses depuis deux ans, c'est le moment. FastAPI 0.128 ne te laisse plus le choix. Et honnetement, une fois la migration faite, tu ne voudras pas revenir. Pydantic v2 est plus rapide, plus explicite, et plus previsible. C'est un meilleur outil.
Commence par bump-pydantic --diff . pour voir l'ampleur des changements. Si c'est moins de 50 fichiers, tu peux tout faire en une session. Au-dela, decoupe par module et migre incrementalement.
Ressources
- Pydantic v2 Migration Guide -- guide officiel de migration
- bump-pydantic -- outil de migration automatique
- FastAPI 0.128 Changelog -- notes de version
- Pydantic v2 Performance -- benchmarks detailles
- Samuel Colvin - Why Pydantic v2 -- conference du createur sur les motivations de la v2