Сьогодні поговоримо про те, як ми можемо використовувати 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! Не забувайте, що коректні відповіді — це запорука довіри, успішної інтеграції і, звісно, вашого спокою. Удачі і менше багів!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ