Комментарий пользователя
Здравствуйте! Обновили pydantic до 2.x, и теперь часть запросов от клиента сразу падает с 422‑ошибками. Вместо Optional стали падать «field required», root_validator не работает, а старые валидаторы вообще не видит. Можно ли как-то перейти на v2, не переписывая весь код сразу?
Ответ специалиста
Добрый день, Никита! Мы регулярно такое видим, особенно на таких сервисах как FastAPI, где много моделей и валидаторов. Во второй версии Pydantic поведение и правда поменялось, но почти все можно сделать по‑человечески. Рефакторинг не будет слишком болезненным.
Ниже привел примеры самого частого, что цепляет прод.
Optional. Он перестает означать «можно не передавать поле» — в v2 такой тип скорее означает «поле обязательно, но может быть None». Если клиент иногда пропускает поле в запросе, в v2 он может вдруг начать получать field required, просто потому что модель ожидает его наличия.
Валидаторы переехали. validator и root_validator из v1 в чистых v2‑моделях не работают, зато есть field_validator и model_validator(mode=”after”) — по сути те же функции, но с другим синтаксисом и чуть более строгой логикой.
Новые методы. model.dict(), parse_obj() и schema() почти полностью заменены на model.model_dump(), model.model_validate() и model.model_json_schema() — это уже норма для новых проектов.
По опыту, самый мягкий путь — это не пытаться в один день переключить весь код, а плавно переносить куски в v2‑стиль, а старые части оставить в pydantic.v1. Так вы не уроните прод, пока тестируете новое поведение.
Вот как может выглядеть переход одной простой модели:
from typing import Optional
from pydantic import BaseModel, field_validator, model_validator, ConfigDict
from typing_extensions import Self
Было в v1:
"""
from pydantic import BaseModel, validator, root_validator
class UserIn(BaseModel):
name: str
email: Optional[str]
age: int
@validator("email")
def ensure_email(cls, v):
if v and "@" not in v:
raise ValueError("Invalid email")
return v
@root_validator
def ensure_age_positive(cls, values):
if values.get("age", 0) < 0:
raise ValueError("Age must be positive")
return values
"""
Стало в v2:
class UserIn(BaseModel):
model_config = ConfigDict(
ser_json_timedelta="iso8601",
extra="forbid",
)
name: str
email: Optional[str] = None # теперь можно честно не передавать
age: int
@field_validator("email")
@classmethod
def validate_email(cls, v: Optional[str]) -> Optional[str]:
if v is not None and "@" not in v:
raise ValueError("Invalid email")
return v
@model_validator(mode="after")
def validate_age_positive(self) -> Self:
if self.age < 0:
raise ValueError("Age must be positive")
return self
Теперь:
- email можно спокойно не передавать в JSON;
- model_validator срабатывает уже после валидации полей;
- в контроллерах используете model.model_validate() и model.model_dump() вместо старых вызовов — это уже привычный паттерн в FastAPI‑проектах.
Если у вас уже есть v1‑слой в проекте, он не сломается сам по себе при обновлении, но это временная мера, пока не сделан ручной или автоматический перенос. Автоматизировать часть миграции можно, например, через bump‑pydantic — он сам переписывает большую часть методов, а вы уже смотрите diff и доводите код до порядка.
Раскатывать изменения лучше постепенно через feature-флаги, переключая на v2-модели по одному роуту. При старте сервиса полезно выводить в лог model_json_schema(), чтобы сразу увидеть, не разъехалась ли схема с тем, что ждет фронтенд.
В K8s на время миграции стоит накинуть лимитов по CPU и памяти. Хоть v2 и быстрее, при смешанном коде нагрузка может скакать, так что лучше перестраховаться.
Оставайтесь на связи с актуальным стеком. А за порцией вдохновения и советов приходите в Академию Selectel.