Сегодня мы поговорим о том, как мы можем использовать Pydantic для валидации данных не только на входе (в запросах), но и на выходе — в том, что наше API отправляет пользователю в виде ответа.
Эта тема не менее важна, чем валидация входных данных. Если API возвращает неожиданные или некорректные данные, это может привести к сломанному интерфейсу, разрыву автоматизации, гневным комментариям от коллег — и, чего уж там, к утрате доверия к вашему API. С таким вы, конечно, сталкиваться не хотите.
Представьте, что вы построили сложную систему, где ваш API взаимодействует с фронтендом или другими сервисами. Что произойдёт, если вы случайно вернёте объект не в том формате, который ожидают ваши клиенты? Например:
- Фронтенд может перестать отображать данные из-за неправильного формата.
- Партнёры, которые используют ваше API, могут начать бить тревогу (и разрывать контракты).
- Ошибки сложнее отлаживать (особенно ночью, когда единственная поддержка — это ваша кружка кофе).
корректный и ожидаемый формат ответа — это лицо вашего API. Именно поэтому валидация ответов так важна.
Как FastAPI помогает с валидацией выходных данных?
FastAPI интегрирован с Pydantic настолько глубоко, что готов автоматически валидировать данные, которые возвращаются из вашего эндпоинта. Чтобы включить эту магию, достаточно указать, какой Pydantic-моделью должен быть представлен ответ.
Создадим первый пример.
Давайте начнём с простого API, которое возвращает информацию о пользователе:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
# Определяем Pydantic-модель
class UserResponse(BaseModel):
id: int
name: str
email: str
@app.get("/user/{user_id}", response_model=UserResponse)
async def get_user(user_id: int):
# Данные из базы данных (или другого источника)
user = {"id": user_id, "name": "Иван Иванов", "email": "ivan@example.com"}
return user
Что происходит в этом коде?
- Мы создали модель
UserResponse, которая описывает, как должны выглядеть возвращаемые данные:id(целое число),name(строка) иemail(строка). - В эндпоинте
/user/{user_id}мы указалиresponse_model=UserResponse. Это говорит FastAPI: "Все данные, которые возвращаются из этого эндпоинта, должны соответствовать моделиUserResponse". - FastAPI автоматически проверит данные перед тем, как отправить их пользователю.
Проверка валидации
Что произойдёт, если мы вернём данные, не соответствующие модели? Например, удалим поле email:
@app.get("/user/{user_id}", response_model=UserResponse)
async def get_user(user_id: int):
user = {"id": user_id, "name": "Иван Иванов"} # Email отсутствует
return user
FastAPI выбросит ошибку ValueError, потому что поле email является обязательным, но его нет в возвращаемых данных.
Преобразование данных через Pydantic
Pydantic не только валидирует, но и автоматически преобразует данные. Например, если вы вернёте строку для поля id, но оно ожидает целое число, Pydantic постарается "исправить" это.
Пример преобразования
@app.get("/user/{user_id}", response_model=UserResponse)
async def get_user(user_id: int):
user = {"id": str(user_id), "name": "Иван Иванов", "email": "ivan@example.com"} # id как строка
return user
Несмотря на то, что id здесь представлен строкой, Pydantic преобразует его в целое число, потому что модель ожидает int. Это удобно, когда данные из разных источников имеют разные типы.
Частичная валидация (поле с типом Any)
Иногда структура ответа может быть сложной или динамической, и вы не хотите прописывать все поля модели. Для таких случаев есть поле с типом Any.
from typing import Any
class FlexibleResponse(BaseModel):
id: int # Важное поле типа int
data: Any # А здесь может быть что угодно
Поля типа Any пропустят любые данные без дополнительной проверки.
Вложенные модели в ответах
Иногда в ответах могут быть сложные вложенные структуры данных. Например, пользователь может иметь список ролей, каждая из которых описана своей моделью.
Пример вложенной модели
from typing import List
class Role(BaseModel):
id: int
name: str
class UserResponseWithRoles(BaseModel):
id: int
name: str
email: str
roles: List[Role] # Список ролей
@app.get("/user/{user_id}/roles", response_model=UserResponseWithRoles)
async def get_user_with_roles(user_id: int):
user = {
"id": user_id,
"name": "Иван Иванов",
"email": "ivan@example.com",
"roles": [{"id": 1, "name": "Администратор"}, {"id": 2, "name": "Пользователь"}],
}
return user
FastAPI проверит не только структуру основного объекта, но и каждого элемента списка roles. Если хоть одна роль окажется некорректной, весь ответ будет признан недействительным.
Дополнительные параметры: response_model_exclude и response_model_include
Иногда вы не хотите возвращать все поля модели (например, исключить конфиденциальную информацию). Для этого существуют параметры response_model_exclude и response_model_include.
Пример с исключением полей
@app.get("/user/{user_id}", response_model=UserResponse, response_model_exclude={"email"})
async def get_user_without_email(user_id: int):
user = {"id": user_id, "name": "Иван Иванов", "email": "ivan@example.com"}
return user
В данном случае поле email не будет включено в ответ.
Отладка ошибок валидации (углубляемся!)
Если данные, возвращаемые вашим эндпоинтом, не проходят валидацию, FastAPI покажет детальное сообщение об ошибке. Например:
Ошибка может выглядеть так:
{
"detail": [
{
"loc": ["response", "email"],
"msg": "field required",
"type": "value_error.missing"
}
]
}
В данном примере FastAPI говорит, что в возвращаемых данных отсутствует поле email. Это поможет вам быстро найти и исправить проблему.
Практическое задание
- Создайте эндпоинт
/product/{product_id}, который возвращает описание продукта. - Используйте вложенные модели: продукт должен содержать поля
id,nameиattributes, гдеattributes— сложный объект с полямиcolor,sizeиin_stock(булево значение). - Напишите API, которое возвращает список продуктов (используйте список
List).
Теперь вы вооружены знаниями, чтобы ваши ответы были такими же идеальными, как архитектура FastAPI! Не забывайте, что корректные ответы — это залог доверия, успешной интеграции и, конечно же, вашего спокойствия. Удачи и меньше багов!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ