Форматирование ответов, скрытие полей, статус-коды
Response model определяет, как FastAPI форматирует ответ клиенту. В этой теме вы научитесь скрывать поля, настраивать статус-коды и создавать разные представления одних и тех же данных.
По умолчанию FastAPI возвращает всё, что вернула функция:
@app.post('/users')
def create_user(user: UserCreate):
return {'id': 1, **user.model_dump(), 'password': 'secret123'}Проблема: В ответ попадёт пароль!
Решение: Используйте response_model:
@app.post('/users', response_model=UserResponse)
def create_user(user: UserCreate):
return {'id': 1, **user.model_dump(), 'password': 'secret123'}FastAPI отфильтрует ответ по схеме UserResponse и исключит поле password.
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
from datetime import datetime
app = FastAPI()
class UserCreate(BaseModel):
username: str
email: EmailStr
password: str
class UserResponse(BaseModel):
id: int
username: str
email: str
created_at: datetime
class Config:
from_attributes = True
@app.post('/users', response_model=UserResponse)
def create_user(user: UserCreate):
# В реальности — сохранение в БД
return {
'id': 1,
'username': user.username,
'email': user.email,
'created_at': datetime.now(),
'password': 'secret123' # Не попадёт в ответ!
}Ответ клиенту:
{
"id": 1,
"username": "john",
"email": "john@example.com",
"created_at": "2024-01-01T12:00:00"
}Поле password исключено автоматически.
Иногда нужно скрыть отдельные поля без создания новой модели:
class User(BaseModel):
username: str
email: str
password: str
token: str
@app.get('/users/me', response_model=User, response_model_exclude={'password', 'token'})
def get_current_user():
return {
'username': 'john',
'email': 'john@example.com',
'password': 'secret',
'token': 'abc123'
}Ответ:
{
"username": "john",
"email": "john@example.com"
}| Параметр | Описание | Пример |
|---|---|---|
response_model_exclude | Исключить поля | {'password', 'token'} |
response_model_include | Включить только эти | {'username', 'email'} |
exclude_none=True | Исключить None-значения | response_model_exclude_none=True |
class Item(BaseModel):
id: int
name: str
description: str | None = None
price: float | None = None
@app.get('/items/1', response_model=Item, response_model_exclude_none=True)
def get_item():
return {
'id': 1,
'name': 'Laptop',
'description': None,
'price': None
}Ответ:
{
"id": 1,
"name": "Laptop"
}None-поля исключены из ответа.
FastAPI автоматически устанавливает статус-коды:
200 OK — GET, PUT, PATCH201 Created — POST (нужно указать явно)204 No Content — DELETE (нужно указать явно)from fastapi import FastAPI, status
app = FastAPI()
@app.post('/items', status_code=status.HTTP_201_CREATED)
def create_item(item: ItemCreate):
return {'id': 1, **item.model_dump()}Или числом:
@app.post('/items', status_code=201)
def create_item(item: ItemCreate):
return {'id': 1, **item.model_dump()}status модуль содержит все HTTP-статусы:
from fastapi import status
status.HTTP_200_OK # 200
status.HTTP_201_CREATED # 201
status.HTTP_204_NO_CONTENT # 204
status.HTTP_400_BAD_REQUEST # 400
status.HTTP_404_NOT_FOUND # 404
status.HTTP_500_INTERNAL_SERVER_ERROR # 500Иногда статус зависит от логики:
from fastapi import Response
@app.put('/items/{item_id}')
def update_item(item_id: int, item: ItemUpdate, response: Response):
# Проверяем, существует ли элемент
existing = get_item_from_db(item_id)
if existing:
# Обновляем существующий
return {'id': item_id, **item.model_dump()}
else:
# Создаём новый, меняем статус на 201
response.status_code = status.HTTP_201_CREATED
return {'id': item_id, **item.model_dump()}Хорошая практика — разные модели для разных операций:
class UserBase(BaseModel):
username: str
email: EmailStr
class UserCreate(UserBase):
password: str = Field(..., min_length=8)
class UserUpdate(BaseModel):
username: str | None = None
email: EmailStr | None = None
password: str | None = None
class UserResponse(UserBase):
id: int
created_at: datetime
class Config:
from_attributes = True
class UserInDB(UserResponse):
password_hash: str
is_active: bool@app.post('/users', response_model=UserResponse, status_code=201)
def create_user(user: UserCreate):
# Создаём пользователя
return {'id': 1, 'username': user.username, 'email': user.email, 'created_at': datetime.now()}
@app.get('/users/{user_id}', response_model=UserResponse)
def get_user(user_id: int):
# Получаем пользователя
return {'id': user_id, 'username': 'john', 'email': 'john@example.com', 'created_at': datetime.now()}
@app.put('/users/{user_id}', response_model=UserResponse)
def update_user(user_id: int, user: UserUpdate):
# Обновляем пользователя
return {'id': user_id, 'username': 'john_updated', 'email': 'john@example.com', 'created_at': datetime.now()}class Address(BaseModel):
city: str
street: str
class Post(BaseModel):
id: int
title: str
content: str
class UserResponse(BaseModel):
id: int
username: str
address: Address
posts: list[Post]
class Config:
from_attributes = True
@app.get('/users/{user_id}', response_model=UserResponse)
def get_user(user_id: int):
return {
'id': user_id,
'username': 'john',
'address': {'city': 'Moscow', 'street': 'Tverskaya'},
'posts': [
{'id': 1, 'title': 'Post 1', 'content': '...'},
{'id': 2, 'title': 'Post 2', 'content': '...'}
]
}FastAPI рекурсивно применит модели к вложенным структурам.
При работе с SQLAlchemy или другими ORM объекты — не словари:
# SQLAlchemy модель
class UserDB:
def __init__(self, id, username, email):
self.id = id
self.username = username
self.email = email
db_user = UserDB(id=1, username='john', email='john@example.com')Без from_attributes Pydantic не сможет прочитать атрибуты:
class UserResponse(BaseModel):
id: int
username: str
email: str
class Config:
from_attributes = True # Читать атрибуты объектовТеперь можно вернуть ORM-объект напрямую:
@app.get('/users/{user_id}', response_model=UserResponse)
def get_user(user_id: int):
user = db.query(User).filter(User.id == user_id).first()
return user # FastAPI сам извлечёт атрибутыfrom fastapi import FastAPI, status, HTTPException
from pydantic import BaseModel, EmailStr, Field
from datetime import datetime
from typing import Literal
app = FastAPI()
# Модели
class PostBase(BaseModel):
title: str = Field(..., min_length=5, max_length=200)
content: str = Field(..., min_length=10)
class PostCreate(PostBase):
tags: list[str] = []
status: Literal['draft', 'published'] = 'draft'
class PostUpdate(BaseModel):
title: str | None = Field(None, min_length=5, max_length=200)
content: str | None = Field(None, min_length=10)
status: Literal['draft', 'published'] | None = None
class PostResponse(PostBase):
id: int
tags: list[str]
status: str
created_at: datetime
updated_at: datetime | None = None
class Config:
from_attributes = True
class PostPublic(PostResponse):
"""Публичная модель — без служебных полей"""
author_id: int
views_count: int = 0
# Имитация БД
posts_db = []
@app.post('/posts', response_model=PostResponse, status_code=status.HTTP_201_CREATED)
def create_post(post: PostCreate):
"""Создать пост (доступно авторам)"""
post_id = len(posts_db) + 1
new_post = {
'id': post_id,
**post.model_dump(),
'created_at': datetime.now(),
'updated_at': None,
'author_id': 1, # В реальности — из аутентификации
'views_count': 0
}
posts_db.append(new_post)
return new_post
@app.get('/posts/{post_id}', response_model=PostPublic)
def get_post(post_id: int):
"""Получить пост (публичный endpoint)"""
for post in posts_db:
if post['id'] == post_id:
return post
raise HTTPException(status_code=404, detail="Post not found")
@app.put('/posts/{post_id}', response_model=PostResponse)
def update_post(post_id: int, post_update: PostUpdate):
"""Обновить пост"""
for post in posts_db:
if post['id'] == post_id:
# Обновляем только переданные поля
update_data = post_update.model_dump(exclude_unset=True)
post.update(update_data)
post['updated_at'] = datetime.now()
return post
raise HTTPException(status_code=404, detail="Post not found")
@app.delete('/posts/{post_id}', status_code=status.HTTP_204_NO_CONTENT)
def delete_post(post_id: int):
"""Удалить пост"""
for i, post in enumerate(posts_db):
if post['id'] == post_id:
posts_db.pop(i)
return Response(status_code=status.HTTP_204_NO_CONTENT)
raise HTTPException(status_code=404, detail="Post not found")
@app.get('/posts', response_model=list[PostPublic])
def list_posts(
status_filter: Literal['draft', 'published'] | None = None,
limit: int = 10,
offset: int = 0
):
"""Список постов с фильтрацией"""
posts = posts_db
if status_filter:
posts = [p for p in posts if p['status'] == status_filter]
return posts[offset:offset + limit]class UserResponse(BaseModel):
id: int
email: str
@app.get('/users/1', response_model=UserResponse)
def get_user():
return {'id': 1, 'name': 'John'} # Ошибка: нет email!Проблема: Ответ не соответствует response_model. FastAPI попытается валидировать и может вернуть ошибку.
Решение: Убедитесь, что возвращаемые данные соответствуют модели.
class UserResponse(BaseModel):
id: int
username: str
@app.get('/users/{id}', response_model=UserResponse)
def get_user(id: int):
user = db.query(User).first() # ORM-объект
return user # Ошибка без from_attributes!Решение:
class UserResponse(BaseModel):
id: int
username: str
class Config:
from_attributes = True@app.get('/items')
def get_items(q: str): # Ошибка: q будет прочитан из body, а не query!
...Проблема: Если нет response_model и функция принимает параметры, FastAPI может неправильно определить источник данных.
Решение: Явно укажите источник:
from fastapi import Query
@app.get('/items')
def get_items(q: str = Query(...)):
...@app.get('/simple', response_model=dict, response_model_exclude_none=True)
def get_simple():
return {'a': 1, 'b': None}Проблема: response_model=dict отключает валидацию и документирование.
Решение: Используйте Pydantic-модель или упростите:
@app.get('/simple')
def get_simple():
return {'a': 1} # Просто исключите None в кодеВ следующей теме вы изучите обработку ошибок — HTTPException, кастомные исключения и error handlers.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.