Привет! Валидация входных данных — это один из самых важных аспектов безопасности и надёжности API. Если не проверять данные, которые приходят от клиента, можно получить неожиданные ошибки, уязвимости и повреждённые данные.
В этой статье мы разберём:
- Зачем нужна валидация
- Проверка типов данных
- Обязательные поля
- Формат данных
- Кастомные проверки
- Валидация в FastAPI и Django REST Framework
- Обработка ошибок валидации
- Что нужно знать перед началом
- Основная часть
- Зачем нужна валидация
- Типы валидации
- 1. Проверка типа данных
- 2. Обязательные поля
- 3. Формат данных
- 4. Кастомные проверки
- Валидация в FastAPI
- Валидация в Django REST Framework
- Валидация вручную (без фреймворков)
- Часто используемые проверки
- Проверка на пустое значение
- Проверка на допустимые значения (enum)
- Проверка формата (регулярные выражения)
- Задачи для закрепления
- Нюансы и подводные камни
- Валидация не заменяет санитизацию
- Преобразование типов
- Ошибки валидации
- Частые ошибки и как их избежать
- Ошибка 1: Игнорирование валидации на сервере
- Ошибка 2: Слишком строгая валидация
- Ошибка 3: Непонятные сообщения об ошибках
- Ошибка 4: Валидация только на уровне модели
- Шпаргалка
- Заключение
- КВИЗ
- Что дальше?
Что нужно знать перед началом
Для этого урока тебе понадобится:
- Установленный Python
- Базовое понимание API и веб-разработки
Совет: Валидация — это первая линия защиты вашего API. Никогда не доверяй данным от клиента без проверки.
Основная часть
Зачем нужна валидация
Валидация входных данных решает несколько задач:
- Безопасность — защита от вредоносных данных (SQL-инъекции, XSS).
- Целостность — гарантия, что данные сохранены в правильном формате.
- Обратная связь — понятные сообщения об ошибках для клиента.
# Без валидации — опасно!
@app.post("/users")
def create_user(name: str, age: int):
# Если age — строка, может возникнуть ошибка
return {"user": f"{name} ({age})"}
# С валидацией — безопасно!
class UserCreate(BaseModel):
name: str = Field(..., min_length=2, max_length=50)
age: int = Field(..., ge=0, le=120)Совет: Всегда валидируй данные на сервере, даже если валидация есть на клиенте.
Типы валидации
1. Проверка типа данных
from pydantic import BaseModel
class User(BaseModel):
name: str # Должна быть строка
age: int # Должно быть число
is_active: bool # Должен быть булев тип
height: float # Должно быть дробное число
# Автоматическая проверка типов
user = User(name=123, age="25", is_active=1)
print(user) # name="123" (преобразовано), age=25 (преобразовано), is_active=TrueСовет: Pydantic автоматически преобразует типы, если это возможно.
2. Обязательные поля
from pydantic import BaseModel, Field
from typing import Optional
class User(BaseModel):
name: str = Field(..., description="Обязательное поле") # ... = обязательно
email: str = Field(..., pattern=r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
age: Optional[int] = None # Необязательное поле3. Формат данных
from pydantic import BaseModel, Field
from datetime import date
class Event(BaseModel):
name: str
date: date # Автоматически проверяет формат даты
price: float = Field(..., gt=0, description="Цена должна быть больше 0")
tags: list[str] = Field(default_factory=list)4. Кастомные проверки
from pydantic import BaseModel, validator
class User(BaseModel):
name: str
password: str
confirm_password: str
@validator('password')
def validate_password(cls, value):
if len(value) < 8:
raise ValueError('Пароль должен быть не менее 8 символов')
if not any(c.isdigit() for c in value):
raise ValueError('Пароль должен содержать хотя бы одну цифру')
return value
@validator('confirm_password')
def validate_confirm(cls, value, values):
if 'password' in values and value != values['password']:
raise ValueError('Пароли не совпадают')
return valueСовет: Валидаторы выполняются в порядке объявления полей.
Валидация в FastAPI
FastAPI использует Pydantic для автоматической валидации.
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field, validator
from typing import Optional
app = FastAPI()
class UserCreate(BaseModel):
name: str = Field(..., min_length=2, max_length=50)
email: str = Field(..., pattern=r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
age: int = Field(..., ge=0, le=120)
password: str = Field(..., min_length=8)
confirm_password: str
@validator('password')
def validate_password(cls, value):
if not any(c.isupper() for c in value):
raise ValueError('Пароль должен содержать хотя бы одну заглавную букву')
return value
@validator('confirm_password')
def validate_confirm(cls, value, values):
if 'password' in values and value != values['password']:
raise ValueError('Пароли не совпадают')
return value
@app.post("/users")
def create_user(user: UserCreate):
# Автоматическая валидация уже выполнена
return {"message": "Пользователь создан", "user": user}Обработка ошибок валидации:
from fastapi import FastAPI, HTTPException
from pydantic import ValidationError
@app.post("/users")
def create_user(user: UserCreate):
try:
# ... логика
return {"message": "Пользователь создан"}
except ValidationError as e:
raise HTTPException(status_code=422, detail=e.errors())Совет: FastAPI автоматически возвращает 422 при ошибках валидации. Кастомная обработка нужна только для дополнительной логики.
Валидация в Django REST Framework
Django REST Framework использует сериализаторы для валидации.
from rest_framework import serializers
from .models import User
class UserSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, min_length=8)
confirm_password = serializers.CharField(write_only=True)
class Meta:
model = User
fields = ['id', 'username', 'email', 'age', 'password', 'confirm_password']
def validate_email(self, value):
if not "@" in value:
raise serializers.ValidationError("Неверный формат email")
return value
def validate_age(self, value):
if value < 0 or value > 120:
raise serializers.ValidationError("Возраст должен быть от 0 до 120")
return value
def validate(self, data):
if data.get('password') != data.get('confirm_password'):
raise serializers.ValidationError("Пароли не совпадают")
return data
def create(self, validated_data):
validated_data.pop('confirm_password')
return User.objects.create_user(**validated_data)Валидация вручную (без фреймворков)
from typing import Dict, Any, Optional
def validate_user(data: Dict[str, Any]) -> tuple[bool, Optional[Dict[str, str]]]:
errors = {}
# Проверка имени
name = data.get('name')
if not name:
errors['name'] = 'Имя обязательно'
elif len(name) < 2 or len(name) > 50:
errors['name'] = 'Имя должно быть от 2 до 50 символов'
# Проверка email
email = data.get('email')
if not email:
errors['email'] = 'Email обязателен'
elif '@' not in email:
errors['email'] = 'Неверный формат email'
# Проверка возраста
age = data.get('age')
if age is None:
errors['age'] = 'Возраст обязателен'
elif not isinstance(age, int) or age < 0 or age > 120:
errors['age'] = 'Возраст должен быть от 0 до 120'
if errors:
return False, errors
return True, None
# Использование
data = {"name": "Анна", "email": "anna@test.com", "age": 25}
is_valid, errors = validate_user(data)
if is_valid:
print("Данные валидны")
else:
print("Ошибки:", errors)Часто используемые проверки
Проверка на пустое значение
# FastAPI (Pydantic)
field: str = Field(..., min_length=1)
# DRF (сериализатор)
field = serializers.CharField(required=True, allow_blank=False)
# Вручную
if not value or value.strip() == "":
raise ValueError("Поле не может быть пустым")Проверка на допустимые значения (enum)
from enum import Enum
class Status(str, Enum):
ACTIVE = "active"
INACTIVE = "inactive"
PENDING = "pending"
# FastAPI
class Item(BaseModel):
status: Status
# DRF
STATUS_CHOICES = [("active", "Active"), ("inactive", "Inactive")]
field = serializers.ChoiceField(choices=STATUS_CHOICES)Проверка формата (регулярные выражения)
import re
def validate_phone(value):
pattern = r"^\+?[1-9]\d{1,14}$"
if not re.match(pattern, value):
raise ValueError("Неверный формат телефона")
return valueЗадачи для закрепления
Задача 1. Создай модель для регистрации с валидацией email.
Задача 2. Добавь валидацию возраста (0–120).
Задача 3. Создай валидатор для пароля (мин. 8 символов, цифра, заглавная буква).
Задача 4. Добавь проверку, что пароль и подтверждение совпадают.
Задача 5. Обработай ошибки валидации и верни понятные сообщения клиенту.
Ответы:
Задача 1.
class Register(BaseModel):
email: str = Field(..., pattern=r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")Задача 2.
age: int = Field(..., ge=0, le=120)Задача 3.
@validator('password')
def validate_password(cls, value):
if len(value) < 8:
raise ValueError('Слишком короткий')
if not any(c.isdigit() for c in value):
raise ValueError('Нужна цифра')
if not any(c.isupper() for c in value):
raise ValueError('Нужна заглавная буква')
return valueЗадача 4.
@validator('confirm_password')
def validate_confirm(cls, value, values):
if 'password' in values and value != values['password']:
raise ValueError('Пароли не совпадают')
return valueЗадача 5.
try:
user = UserCreate(**data)
except ValidationError as e:
return {"errors": e.errors()}, 422Нюансы и подводные камни
Валидация не заменяет санитизацию
Валидация проверяет данные, но не очищает их. Для защиты от XSS используй экранирование или санитизацию.
Преобразование типов
Pydantic автоматически преобразует типы. Это удобно, но может привести к неожиданным результатам (например, "25" → 25).
Ошибки валидации
Возвращай понятные сообщения об ошибках, чтобы клиент мог их исправить.
# Плохо
{"detail": "Validation error"}
# Хорошо
{"errors": {"email": "Неверный формат email", "age": "Возраст должен быть числом"}}Частые ошибки и как их избежать
Ошибка 1: Игнорирование валидации на сервере
Всегда валидируй данные на сервере, даже если есть валидация на клиенте.
Ошибка 2: Слишком строгая валидация
Учитывай реальные сценарии использования. Не делай валидацию слишком жёсткой.
Ошибка 3: Непонятные сообщения об ошибках
Возвращай конкретные сообщения с указанием поля и причины ошибки.
Ошибка 4: Валидация только на уровне модели
Валидируй данные на уровне API, чтобы отсекать неверные данные на раннем этапе.
Шпаргалка
| Что нужно | FastAPI (Pydantic) | DRF (сериализатор) | Вручную |
|---|---|---|---|
| Обязательное поле | Field(...) | required=True | if not value: |
| Минимальная длина | min_length=2 | min_length=2 | len(value) < 2 |
| Максимальная длина | max_length=50 | max_length=50 | len(value) > 50 |
| Регулярное выражение | pattern=r"..." | regex=r"..." | re.match() |
| Кастомная проверка | @validator | validate_<field> | Функция |
Заключение
Сегодня мы:
- Узнали, зачем нужна валидация
- Разобрали типы валидации
- Реализовали валидацию в FastAPI и DRF
- Примеры и частые ошибки








