← Назад к вопросам

Зачем нужен валидатор в pydantic?

2.0 Middle🔥 191 комментариев
#Асинхронность и многопоточность#Базы данных (NoSQL)

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Валидаторы в Pydantic: назначение и применение

Валидаторы в Pydantic — это функции, которые проверяют и трансформируют данные перед созданием объекта модели. Это одна из самых мощных фич Pydantic, которая часто недоиспользуется. За 10+ лет я вижу, что правильное использование валидаторов экономит часы отладки и повышает безопасность приложения.

Что делают валидаторы?

Валидаторы решают две проблемы:

  1. Проверка данных — убедиться, что данные соответствуют бизнес-правилам
  2. Трансформация — преобразовать данные в нужный формат

Pydantic v2: использование field_validator

В Pydantic v2 используется декоратор @field_validator:

from pydantic import BaseModel, field_validator
from typing import Optional

class User(BaseModel):
    name: str
    email: str
    age: int
    
    @field_validator('name')
    @classmethod
    def name_must_not_be_empty(cls, v: str) -> str:
        if not v or v.isspace():
            raise ValueError('name cannot be empty or whitespace')
        return v.strip()  # Трансформация: убираем пробелы
    
    @field_validator('email')
    @classmethod
    def email_must_be_valid(cls, v: str) -> str:
        if '@' not in v:
            raise ValueError('invalid email format')
        return v.lower()  # Трансформация: приводим к нижнему регистру
    
    @field_validator('age')
    @classmethod
    def age_must_be_positive(cls, v: int) -> int:
        if v < 0 or v > 150:
            raise ValueError('age must be between 0 and 150')
        return v

# Использование
try:
    user = User(name='  John  ', email='JOHN@EXAMPLE.COM', age=30)
    print(user)
    # name='John', email='john@example.com', age=30
    # Видим, что данные трансформировались!
except ValueError as e:
    print(f'Validation error: {e}')

Валидация нескольких полей одновременно

Один валидатор может проверять несколько полей:

from pydantic import BaseModel, field_validator
from datetime import datetime

class Event(BaseModel):
    name: str
    start_date: datetime
    end_date: datetime
    
    @field_validator('end_date')
    @classmethod
    def end_date_after_start(cls, v: datetime, info) -> datetime:
        # info.data содержит уже валидированные данные
        start_date = info.data.get('start_date')
        if start_date and v <= start_date:
            raise ValueError('end_date must be after start_date')
        return v

# Плохо: end_date раньше start_date
try:
    event = Event(
        name='Conference',
        start_date=datetime(2026, 3, 22, 10, 0),
        end_date=datetime(2026, 3, 21, 18, 0)  # Раньше!
    )
except ValueError as e:
    print(f'Error: {e}')  # Error: end_date must be after start_date

# Хорошо
event = Event(
    name='Conference',
    start_date=datetime(2026, 3, 22, 10, 0),
    end_date=datetime(2026, 3, 23, 18, 0)  # После
)
print(event)  # OK

Кастомные валидаторы: mode='before' vs 'after'

from pydantic import BaseModel, field_validator

class Product(BaseModel):
    name: str
    price: float
    quantity: int
    
    @field_validator('price', mode='before')
    @classmethod
    def convert_price(cls, v):
        # Выполняется ДО преобразования типа
        if isinstance(v, str):
            # "100.50" -> 100.50
            return float(v.replace('$', ''))
        return v
    
    @field_validator('quantity', mode='after')
    @classmethod
    def quantity_non_negative(cls, v: int) -> int:
        # Выполняется ПОСЛЕ преобразования типа
        if v < 0:
            raise ValueError('quantity must be >= 0')
        return v

# before валидатор срабатывает первым
product = Product(name='Widget', price='$99.99', quantity=-5)
# Валидация price: "$99.99" -> 99.99 ✓
# Но quantity валидация упадёт на -5

Практический пример: валидация URL и данных

from pydantic import BaseModel, field_validator, HttpUrl
from typing import Optional
import re

class BlogPost(BaseModel):
    title: str
    slug: str
    content: str
    url: HttpUrl
    tags: list[str]
    
    @field_validator('title')
    @classmethod
    def title_length(cls, v: str) -> str:
        if len(v) < 3 or len(v) > 200:
            raise ValueError('title length must be 3-200 characters')
        return v.strip()
    
    @field_validator('slug')
    @classmethod
    def slug_format(cls, v: str) -> str:
        # Slug должен быть только буквы, цифры, дефисы
        if not re.match(r'^[a-z0-9-]+$', v):
            raise ValueError('slug must contain only lowercase letters, digits, and hyphens')
        return v
    
    @field_validator('content')
    @classmethod
    def content_minimum_length(cls, v: str) -> str:
        if len(v) < 100:
            raise ValueError('content must be at least 100 characters')
        return v
    
    @field_validator('tags')
    @classmethod
    def tags_validation(cls, v: list[str]) -> list[str]:
        # Каждый тег должен быть 2-20 символов
        for tag in v:
            if len(tag) < 2 or len(tag) > 20:
                raise ValueError(f'tag "{tag}" length must be 2-20 characters')
        # Трансформация: приводим к нижнему регистру
        return [tag.lower() for tag in v]

# Использование
post = BlogPost(
    title='My First Post',
    slug='my-first-post',
    content='This is a very long content with more than 100 characters...' * 2,
    url='https://example.com/post',
    tags=['Python', 'FastAPI', 'Web']
)
print(post.tags)  # ['python', 'fastapi', 'web'] — уже в нижнем регистре

Валидаторы для корневого объекта

from pydantic import BaseModel, model_validator

class Payment(BaseModel):
    amount: float
    discount: float
    
    @model_validator(mode='after')
    def discount_cannot_exceed_amount(self) -> 'Payment':
        # Валидирует весь объект после создания
        if self.discount > self.amount:
            raise ValueError('discount cannot exceed amount')
        return self

payment = Payment(amount=100, discount=150)  # Ошибка!
# ValueError: discount cannot exceed amount

payment = Payment(amount=100, discount=20)  # OK
print(payment)  # amount=100.0 discount=20.0

Валидация с зависимостями между полями

from pydantic import BaseModel, field_validator
from typing import Optional

class Address(BaseModel):
    country: str
    state: Optional[str] = None
    zip_code: Optional[str] = None
    
    @field_validator('state', 'zip_code')
    @classmethod
    def us_fields_required(cls, v, info):
        # Если страна США, то state и zip_code обязательны
        if info.data.get('country') == 'USA' and v is None:
            field_name = info.field_name
            raise ValueError(f'{field_name} required for USA addresses')
        return v

# Ошибка: США требует state и zip_code
try:
    addr = Address(country='USA', state=None, zip_code=None)
except ValueError as e:
    print(e)  # state required for USA addresses

# OK: Другие страны не требуют
addr = Address(country='Russia')  # OK
print(addr)  # country='Russia' state=None zip_code=None

Какая разница между валидаторами и type hints?

from pydantic import BaseModel, field_validator

class Data(BaseModel):
    # Type hint: только проверка типа (автоматически)
    age: int  # Pydantic проверит: это int или можно конвертировать?
    
    # Валидатор: проверка бизнес-логики
    @field_validator('age')
    @classmethod
    def age_range(cls, v: int) -> int:
        if v < 0 or v > 150:
            raise ValueError('invalid age')
        return v

# Type hint автоматически:
data = Data(age='25')  # Конвертирует string -> int
print(data.age)  # 25 (int)

# Валидатор проверяет бизнес-правило:
data = Data(age=200)  # Type hint пройдёт, но валидатор упадёт
# ValueError: invalid age

Когда использовать валидаторы

✅ Используй валидаторы для:

  • Проверки бизнес-правил (диапазоны, соотношения)
  • Трансформации данных (trim, lowercase, parsing)
  • Кросс-полевой валидации (одно поле зависит от другого)
  • Защиты от инъекций и некорректных форматов
  • Документирования ожиданий через код

❌ Не используй валидаторы для:

  • Простой проверки типа (это делают type hints)
  • Сложных запросов в БД (это должно быть в application layer)
  • Бизнес-логики, которая должна быть в services

Заключение

Валидаторы в Pydantic — это граница между ненадежными внешними данными и чистым приложением. Они:

  • Автоматически проверяют данные при создании объекта
  • Трансформируют данные в нужный формат
  • Выполняют бизнес-правила на уровне модели
  • Документируют ожидания через код

Правильное использование валидаторов — это признак профессионального разработчика, который заботится о качестве и безопасности.

Зачем нужен валидатор в pydantic? | PrepBro