Валидация входных данных в API

Валидация входных данных в API REST API

Привет! Валидация входных данных — это один из самых важных аспектов безопасности и надёжности API. Если не проверять данные, которые приходят от клиента, можно получить неожиданные ошибки, уязвимости и повреждённые данные.

В этой статье мы разберём:

  • Зачем нужна валидация
  • Проверка типов данных
  • Обязательные поля
  • Формат данных
  • Кастомные проверки
  • Валидация в FastAPI и Django REST Framework
  • Обработка ошибок валидации

Что нужно знать перед началом

Для этого урока тебе понадобится:

  • Установленный Python
  • Базовое понимание API и веб-разработки

Совет: Валидация — это первая линия защиты вашего API. Никогда не доверяй данным от клиента без проверки.

Основная часть

Зачем нужна валидация

Валидация входных данных решает несколько задач:

  1. Безопасность — защита от вредоносных данных (SQL-инъекции, XSS).
  2. Целостность — гарантия, что данные сохранены в правильном формате.
  3. Обратная связь — понятные сообщения об ошибках для клиента.
# Без валидации — опасно!
@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=Trueif not value:
Минимальная длинаmin_length=2min_length=2len(value) < 2
Максимальная длинаmax_length=50max_length=50len(value) > 50
Регулярное выражениеpattern=r"..."regex=r"..."re.match()
Кастомная проверка@validatorvalidate_<field>Функция

Заключение

Сегодня мы:

  • Узнали, зачем нужна валидация
  • Разобрали типы валидации
  • Реализовали валидацию в FastAPI и DRF
  • Примеры и частые ошибки

КВИЗ

Что дальше?

Оцените статью
IMI-DS - PYTHON LERNEN
Содержание
Оглавление ×