Response-модели и разделение входа и выхода
response_model описывает форму ответа: FastAPI фильтрует и валидирует возвращаемые данные по этой модели, что позволяет не отдавать наружу лишнее и чувствительное.
Главный принцип: модель входа и модель выхода — это разные модели. Клиент присылает пароль, но в ответ его возвращать нельзя.
response_modelгарантирует, что наружу уйдёт только разрешённое.
Соблазн использовать одну модель и для приёма, и для отдачи данных велик, но опасен. На входе вам нужен пароль, согласие с офертой, временные технические поля. На выходе — публичная проекция: id, имя, дата регистрации, но никак не хеш пароля. Если отдавать ту же модель, легко случайно «протечь» секретами. Поэтому в FastAPI принято заводить как минимум две модели и указывать выходную через response_model.
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserIn(BaseModel): # вход: есть пароль
email: EmailStr
password: str
class UserOut(BaseModel): # выход: пароля нет
id: int
email: EmailStr
@app.post("/users", response_model=UserOut)
async def create_user(user: UserIn):
saved = {"id": 1, "email": user.email, "password": user.password}
return saved # FastAPI отфильтрует словарь по UserOut и НЕ вернёт password
Даже если обработчик вернёт словарь с лишними полями, FastAPI прогонит его через UserOut и оставит только id и email. Это работает как фильтр и страховка. Дополнительно response_model_exclude_none=True убирает из ответа поля со значением None, а response_model_exclude/include точечно управляют составом.
Как работает под капотом
FastAPI берёт то, что вернул обработчик, и валидирует это уже как выходную модель — то есть строит из результата экземпляр response_model и сериализует только его поля. Лишние ключи отбрасываются. Смоделируем фильтрацию ответа на stdlib:
def project(data, allowed_fields):
# оставляем только поля, описанные в response_model
return {k: data[k] for k in allowed_fields if k in data}
# то, что вернул обработчик (есть лишнее и секретное)
raw = {"id": 1, "email": "[email protected]", "password": "supersecret", "internal_flag": True}
allowed = ["id", "email"] # поля UserOut
response = project(raw, allowed)
print("уйдёт клиенту:", response)
print("password просочился?", "password" in response)
Попробуй сам ▶ Поле password отсеяно — именно так response_model защищает от утечки чувствительных данных.
Частые ошибки
Первая и опасная — одна модель на вход и выход, из-за чего секреты утекают в ответ. Вторая — думать, что возврат «лишних» полей вызовет ошибку: FastAPI их просто отфильтрует, поэтому утечку легко не заметить без явной выходной модели. Третья — забывать про response_model там, где это важно для контракта и документации. Четвёртая — пытаться валидацией ответа чинить ошибки логики: response_model не для бизнес-правил, а для формы данных.
Best practices
- Всегда разделяйте модели:
XxxInдля входа,XxxOutдля выхода. - Указывайте
response_modelявно — это контракт ответа и документация. - Никогда не включайте пароли, токены, внутренние флаги в выходные модели.
- Управляйте составом ответа через
exclude_none,exclude,includeпри необходимости.
Наследование моделей: Base, Create, Read
Чтобы не дублировать поля между входными и выходными моделями, применяют наследование. Заводят базовую модель с общими полями (ItemBase с name и price), от неё — входную ItemCreate, добавляющую то, что нужно только при создании, и выходную ItemRead, добавляющую серверные поля вроде id и created_at. Этот шаблон Base/Create/Read стал де-факто стандартом: общие поля описаны один раз, а различия видны явно. Он отлично сочетается с from_attributes=True в выходной модели, позволяя строить ответ прямо из ORM-объекта. Дисциплина «у каждой операции своя модель, общее — в базовой» делает контракт API читаемым и устойчивым к ошибкам: добавив поле в базу, вы автоматически получаете его и на входе, и на выходе, не рискуя забыть про одну из моделей.
Итог: response_model валидирует и фильтрует ответ, защищая от утечки лишнего. Разделение входных и выходных моделей — обязательная практика для безопасного API.