Skip to content

Request / Response Modeling

In FastAPI, schemas are not just validation helpers. They are the public contract of your API. Once request DTOs, internal commands, ORM entities, and response DTOs collapse into one type, versioning, security, lazy loading, and serialization costs all get tangled together.

Quick takeaway: treat `request model != response model != ORM model` as the default. Explicit inputs and explicit outputs produce APIs that evolve much more safely.

The Boundary Model

Separating request DTOs, service inputs, ORM records, and response DTOs keeps API evolution and persistence evolution from becoming the same problem.

Baseline Pattern

py
from datetime import datetime
from typing import Annotated
from uuid import UUID

from fastapi import Query
from pydantic import BaseModel, ConfigDict, EmailStr, Field


class CreateUserRequest(BaseModel):
    email: EmailStr
    name: str = Field(min_length=1, max_length=80)


class UserResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: UUID
    email: EmailStr
    name: str
    created_at: datetime


PageSize = Annotated[int, Query(ge=1, le=100)]

Why Request and Response Models Should Differ

  • Create requests may contain write-only fields like passwords or invitation codes.
  • Responses need server-owned fields such as IDs, timestamps, and status.
  • ORM records may have persistence-only fields like password_hash, soft-delete columns, or version counters.

response_model Is Both Filter and Contract

py
@router.post(
    "/users",
    response_model=UserResponse,
    status_code=201,
)
async def create_user(payload: CreateUserRequest) -> UserResponse:
    ...

In FastAPI, `response_model` is not just for documentation. It shapes serialization and narrows the public contract. That is why returning an explicit response DTO is safer than leaking raw ORM entities.

Design Rules for Durable APIs

Split input and output

Do not reuse one schema for create, update, and read if the field meaning differs.

Keep server-owned fields explicit

IDs, timestamps, and version counters usually belong in responses, not requests.

Fix pagination and error shapes early

Stable list and error envelopes save a lot of migration pain later.

Stop ORM leakage

Build DTOs explicitly so serialization never depends on hidden lazy loads.

Practical Rules

  • Use dedicated CreateXRequest, UpdateXRequest, and XResponse models.
  • Keep response DTOs stable even if persistence columns change.
  • Use Annotated plus Query, Path, and Header to make constraints visible in the function signature.
  • Keep list responses consistent with items + page info.

Official Sources

Built with VitePress for a Python 3.14 handbook.