Passer au contenu principal
RM
Retour au blog

FastAPI 0.128 supprime la compatibilité Pydantic v1. Guide de migration concret.

Radnoumane Mossabely8 min read
FastAPI migration Pydantic v2
FastAPI
Pydantic
Python
Migration
Breaking Change
0 vues

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(), ConfigDict remplace class Config, field_serializer remplace @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 :

OperationPydantic v1Pydantic v2Gain
Validation simple (5 champs)12 us0.8 us15x
Validation complexe (nested)85 us3.2 us26x
Serialisation JSON45 us1.1 us40x
Validation de 10K objets120 ms8 ms15x

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()

hljs python
# 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

hljs python
# 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

hljs python
# 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

hljs python
# 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

hljs bash
# 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.

hljs bash
pip install bump-pydantic
bump-pydantic --diff .  # Preview des changements
bump-pydantic .          # Appliquer les changements

L'outil gere automatiquement :

  • parse_obj() vers model_validate()
  • parse_raw() vers model_validate_json()
  • .dict() vers .model_dump()
  • .json() vers .model_dump_json()
  • class Config vers model_config = ConfigDict(...)
  • orm_mode vers from_attributes

Etape 3 : Migrer les validators manuellement

bump-pydantic ne gere pas bien les validators complexes. Tu dois les migrer a la main.

hljs python
# 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.

hljs python
# 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.

hljs python
# 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 :

  1. ConfigError : une option de Config qui n'existe plus en v2
  2. TypeError dans les validators : signature differente entre v1 et v2
  3. Serialisation differente : les types datetime, Decimal, UUID peuvent avoir un format de sortie different
  4. Strict mode : Pydantic v2 est plus strict par defaut sur les coercions de types
hljs python
# 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.

hljs python
# 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.

hljs python
# 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.

hljs python
# 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-pydantic a 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 Optional fields
  • 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

Partager: