Зачем нужен валидатор в pydantic?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Валидаторы в Pydantic: назначение и применение
Валидаторы в Pydantic — это функции, которые проверяют и трансформируют данные перед созданием объекта модели. Это одна из самых мощных фич Pydantic, которая часто недоиспользуется. За 10+ лет я вижу, что правильное использование валидаторов экономит часы отладки и повышает безопасность приложения.
Что делают валидаторы?
Валидаторы решают две проблемы:
- Проверка данных — убедиться, что данные соответствуют бизнес-правилам
- Трансформация — преобразовать данные в нужный формат
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 — это граница между ненадежными внешними данными и чистым приложением. Они:
- Автоматически проверяют данные при создании объекта
- Трансформируют данные в нужный формат
- Выполняют бизнес-правила на уровне модели
- Документируют ожидания через код
Правильное использование валидаторов — это признак профессионального разработчика, который заботится о качестве и безопасности.