Валидация данных в FastAPI

Валидация данных в FastAPI FastAPI

Привет! FastAPI использует Pydantic для валидации данных. Это одна из самых мощных фишек фреймворка — данные автоматически проверяются, преобразуются и документируются.

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

  • Базовую валидацию типов
  • Валидацию через Field
  • Кастомные валидаторы
  • Вложенные модели
  • Обработку ошибок валидации

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

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

  • Установленный FastAPI
  • Базовое понимание Pydantic моделей

Совет: Валидация данных — это гарантия того, что твоё API получает правильные данные.

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

Базовые типы

FastAPI автоматически валидирует типы данных на основе аннотаций.

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class User(BaseModel):
    name: str
    age: int
    is_active: bool = True
    height: float

@app.post("/users/")
def create_user(user: User):
    return {"user": user}

Ошибка при неверном типе:

# Запрос с неверным типом
{
    "name": "Анна",
    "age": "25",        # Должно быть число
    "height": 1.75
}

# Ответ: 422 Unprocessable Entity

Валидация через Field

Field добавляет дополнительные проверки.

from pydantic import BaseModel, Field

class User(BaseModel):
    name: str = Field(..., min_length=2, max_length=50)
    age: int = Field(..., ge=0, le=120)
    email: str = Field(..., regex=r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
    password: str = Field(..., min_length=6)

Параметры Field:

ПараметрНазначение
min_lengthМинимальная длина строки
max_lengthМаксимальная длина строки
geБольше или равно (для чисел)
leМеньше или равно (для чисел)
gtБольше (для чисел)
ltМеньше (для чисел)
regexРегулярное выражение
defaultЗначение по умолчанию
descriptionОписание для документации

Валидация с помощью валидаторов

Для сложной логики используются кастомные валидаторы.

from pydantic import BaseModel, validator

class User(BaseModel):
    name: str
    password: str
    password_confirm: str

    @validator('password')
    def validate_password(cls, value):
        if len(value) < 8:
            raise ValueError('Пароль должен содержать минимум 8 символов')
        if not any(char.isdigit() for char in value):
            raise ValueError('Пароль должен содержать хотя бы одну цифру')
        return value

    @validator('password_confirm')
    def validate_password_confirm(cls, value, values):
        if 'password' in values and value != values['password']:
            raise ValueError('Пароли не совпадают')
        return value

Совет: Валидаторы выполняются в порядке объявления полей.

Валидация списков

from typing import List
from pydantic import BaseModel, Field

class Item(BaseModel):
    name: str
    price: float = Field(..., ge=0)

class Order(BaseModel):
    items: List[Item]
    total: float = Field(..., ge=0)

Валидация вложенных моделей

class Address(BaseModel):
    street: str = Field(..., min_length=1)
    city: str = Field(..., min_length=1)
    zip_code: str = Field(..., regex=r"^\d{6}$")

class User(BaseModel):
    name: str
    age: int = Field(..., ge=0, le=120)
    address: Address

Пример запроса:

{
    "name": "Анна",
    "age": 25,
    "address": {
        "street": "ул. Ленина, 1",
        "city": "Москва",
        "zip_code": "123456"
    }
}

Опциональные поля

from typing import Optional

class User(BaseModel):
    name: str
    age: Optional[int] = None
    city: Optional[str] = None
    phone: Optional[str] = Field(None, min_length=10, max_length=15)

Совет: Optional и значение по умолчанию None делают поле необязательным.

Кастомные ошибки

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, validator

app = FastAPI()

class User(BaseModel):
    name: str
    age: int

    @validator('age')
    def validate_age(cls, value):
        if value < 0:
            raise ValueError('Возраст не может быть отрицательным')
        if value > 150:
            raise ValueError('Слишком большой возраст')
        return value

@app.post("/users/")
def create_user(user: User):
    return {"user": user}

Полный пример

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field, validator
from typing import Optional, List
import re

app = FastAPI()

class Address(BaseModel):
    street: str = Field(..., min_length=1)
    city: str = Field(..., min_length=1)
    zip_code: str = Field(..., regex=r"^\d{6}$")

class UserCreate(BaseModel):
    name: str = Field(..., min_length=2, max_length=50)
    email: str = Field(..., regex=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)
    password_confirm: str
    phone: Optional[str] = Field(None, min_length=10, max_length=15)
    address: Optional[Address] = None
    tags: List[str] = []

    @validator('password')
    def validate_password(cls, value):
        if not any(char.isdigit() for char in value):
            raise ValueError('Пароль должен содержать хотя бы одну цифру')
        if not any(char.isupper() for char in value):
            raise ValueError('Пароль должен содержать хотя бы одну заглавную букву')
        return value

    @validator('password_confirm')
    def validate_password_confirm(cls, value, values):
        if 'password' in values and value != values['password']:
            raise ValueError('Пароли не совпадают')
        return value

    @validator('phone')
    def validate_phone(cls, value):
        if value is not None:
            # Удаляем все нецифровые символы
            clean = re.sub(r'\D', '', value)
            if len(clean) < 10 or len(clean) > 15:
                raise ValueError('Неверный формат телефона')
        return value

@app.post("/users/")
def create_user(user: UserCreate):
    return {
        "message": "Пользователь создан",
        "user": user
    }

Задачи для закрепления

Задача 1. Создай модель Product с валидацией price >= 0.

Задача 2. Добавь валидацию длины строки name (2–50 символов).

Задача 3. Создай валидатор, проверяющий, что end_date > start_date.

Задача 4. Создай модель с опциональным полем description.

Задача 5. Добавь валидацию email с помощью регулярного выражения.

Ответы:

Задача 1.

class Product(BaseModel):
    name: str
    price: float = Field(..., ge=0)

Задача 2.

class Product(BaseModel):
    name: str = Field(..., min_length=2, max_length=50)
    price: float = Field(..., ge=0)

Задача 3.

class Event(BaseModel):
    start_date: str
    end_date: str

    @validator('end_date')
    def validate_dates(cls, value, values):
        if 'start_date' in values and value <= values['start_date']:
            raise ValueError('end_date должен быть позже start_date')
        return value

Задача 4.

class Product(BaseModel):
    name: str
    price: float
    description: Optional[str] = None

Задача 5.

class User(BaseModel):
    email: str = Field(..., regex=r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")

Нюансы и подводные камни

  • FastAPI использует Pydantic для валидации.
  • Валидация выполняется автоматически при получении запроса.
  • Field добавляет правила валидации к полям модели.
  • @validator позволяет создавать кастомные проверки.
  • Валидаторы выполняются в порядке объявления полей.
  • Если валидация не пройдена — возвращается 422.
  • Можно валидировать отдельные поля и комбинации полей.

Частые ошибки и как их избежать

Ошибка 1: Забыл обработать ошибки валидации

Используй try-except для обработки ValidationError.

Ошибка 2: Неправильный порядок валидаторов

Валидаторы выполняются в порядке объявления полей.

Ошибка 3: Использование Field без импорта

from pydantic import Field

Шпаргалка

Что нужноКак пишется
Минимальная длинаField(..., min_length=2)
Максимальная длинаField(..., max_length=50)
Больше или равноField(..., ge=0)
Меньше или равноField(..., le=120)
Регулярное выражениеField(..., regex=r"pattern")
Кастомный валидатор@validator('field')
Опциональное полеfield: Optional[str] = None

Заключение

Сегодня мы:

  • Изучили валидацию данных в FastAPI
  • Использовали Field для проверок
  • Создавали кастомные валидаторы
  • Валидировали вложенные модели

КВИЗ

Что дальше?

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