Modelo de Resposta — Controle Preciso da Saída da API
Um modelo de resposta é como a iluminação de palco—ilumina apenas as áreas que o público deve ver, enquanto mantém os equipamentos dos bastidores (campos internos) no escuro, garantindo tanto estética quanto segurança.
1. O Que Você Vai Aprender
- Noções básicas de
response_model: Filtragem Automática de Campos Não Declarados, Controle de Serialização response_model_exclude/response_model_include: Filtragem refinada em nível de campo- Modelo de múltipla resposta: Um único endpoint retorna modelos diferentes (
Union/resposta de lista) response_model_by_aliaseresponse_model_exclude_unset: Dicas Práticas- Cenário da Alice: Resposta de Preço do PriceTracker—Versão Pública (Preço de Custo Oculto) vs. Versão Admin (Cadeia Completa de Preços)
2. A História Real da Alice
(1) Problema: Preços de custo vazados para concorrentes
O PriceTracker da Alice armazena os preços de varejo e atacado dos produtos. Um dia, quando Bob solicitou detalhes do produto pelo front end, a API retornou os preços de atacado também. Um concorrente fez scraping da API e obteve todas as informações de preço de atacado, causando grande insatisfação dos clientes da Alice. A causa raiz do problema foi que o Flask não possui um mecanismo de filtragem de resposta, e todos os campos do objeto ORM foram serializados e retornados diretamente.
(2) Solução com response_model
Filtragem declarativa do response_model do FastAPI—retorna apenas os campos declarados no modelo, exclui automaticamente campos não declarados, e previne vazamentos de dados em nível de schema.
from fastapi import FastAPI
from pydantic import BaseModel
class ProductPublic(BaseModel):
id: int
name: str
retail_price: float # Apenas preço de varejo
class ProductAdmin(BaseModel):
id: int
name: str
retail_price: float
wholesale_price: float # Inclui atacado
app = FastAPI()
@app.get("/products/{id}", response_model=ProductPublic)
async def get_product_public(id: int):
# Mesmo que o BD tenha wholesale_price, response_model filtra
return {"id": id, "name": "Widget", "retail_price": 29.99, "wholesale_price": 15.0}
(3) Resultado
A filtragem de resposta mudou de "Lembre-se de deletar campos manualmente" para "Filtragem automática baseada em modelos declarados," eliminando completamente o problema de vazamento de preços de atacado. A API pública agora retorna apenas os campos declarados em ProductPublic, enquanto a API de gerenciamento retorna os dados completos usando ProductAdmin.
3. Noções Básicas do response_model
(1) Princípios da Filtragem Automática
flowchart LR
A[ORM Object] --> B{response_model}
B -->|Campos declarados| C[JSON Response]
B -->|Campos não declarados| D[Filtrados]
subgraph ProductPublic
E[id]
F[name]
G[retail_price]
end
subgraph Hidden
H[wholesale_price]
I[cost_price]
J[supplier_id]
end
(1) ▶ Exemplo: Filtragem automática com response_model
from fastapi import FastAPI
from pydantic import BaseModel
class ProductResponse(BaseModel):
id: int
name: str
category: str
# Registro de banco de dados simulado com campos extras
DB_PRODUCT = {
"id": 1,
"name": "Widget",
"category": "electronics",
"cost_price": 8.50, # NÃO deve estar na resposta
"supplier_id": 42, # NÃO deve estar na resposta
}
app = FastAPI()
@app.get("/products/{product_id}", response_model=ProductResponse)
async def get_product(product_id: int):
# cost_price e supplier_id são auto-filtrados
return DB_PRODUCT
Saída:
{"id": 1, "name": "Widget", "category": "electronics"}
(2) Comparação entre response_model e Tipos de Retorno
| Método | Comportamento de Filtragem | Geração de Documentação | Cenários Recomendados |
|---|---|---|---|
response_model=X |
Auto-filtragem | Sim | Sempre recomendado |
Tipo de Retorno -> X |
Sem Filtragem | Sim | Apenas Anotação de Tipo |
| Sem declaração | Sem filtragem | Nenhum | Não recomendado |
(2) ▶ Exemplo: response_model vs. tipo de retorno
from fastapi import FastAPI
from pydantic import BaseModel
class ProductBrief(BaseModel):
id: int
name: str
app = FastAPI()
@app.get("/demo/model", response_model=ProductBrief)
async def with_response_model():
# response_model FILTRA: apenas id e name na resposta
return {"id": 1, "name": "Widget", "secret": "hidden"}
@app.get("/demo/type") -> ProductBrief
async def with_return_type():
# Tipo de retorno NÃO filtra: campo secret vaza!
return {"id": 1, "name": "Widget", "secret": "leaked"}
Saída:
# Função definida com sucesso
4. Filtragem Refinada em Nível de Campo
(1) exclude e include
Quando você não quer criar um novo modelo para cada cenário, pode usar response_model_exclude e response_model_include para filtrar campos dinamicamente.
(1) ▶ Exemplo: exclude para excluir campos sensíveis
from fastapi import FastAPI
from pydantic import BaseModel, Field
class ProductFull(BaseModel):
id: int
name: str
retail_price: float
wholesale_price: float = Field(exclude=True) # Excluir por padrão
cost_price: float = Field(exclude=True)
supplier: str = Field(exclude=True)
app = FastAPI()
@app.get("/products/{id}", response_model=ProductFull)
async def get_product(id: int):
return {
"id": id,
"name": "Widget",
"retail_price": 29.99,
"wholesale_price": 15.0,
"cost_price": 8.50,
"supplier": "Acme Corp",
}
Saída:
INFO: 127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK
Saída (wholesale_price, cost_price, e supplier foram filtrados):
{"id": 1, "name": "Widget", "retail_price": 29.99}
(2) ▶ Exemplo: response_model_exclude Exclusão Dinâmica
from fastapi import FastAPI
from pydantic import BaseModel
class ProductFull(BaseModel):
id: int
name: str
retail_price: float
wholesale_price: float
cost_price: float
app = FastAPI()
@app.get(
"/products/{id}",
response_model=ProductFull,
response_model_exclude={"wholesale_price", "cost_price"},
)
async def get_product_public(id: int):
return {
"id": id, "name": "Widget",
"retail_price": 29.99, "wholesale_price": 15.0, "cost_price": 8.50,
}
@app.get(
"/admin/products/{id}",
response_model=ProductFull,
)
async def get_product_admin(id: int):
return {
"id": id, "name": "Widget",
"retail_price": 29.99, "wholesale_price": 15.0, "cost_price": 8.50,
}
Saída:
# Função definida com sucesso
| Método de Filtragem | Cenários Aplicáveis | Granularidade |
|---|---|---|
response_model=ModelA |
Modelos Diferentes de Perspectivas Diferentes | Nível de Modelo |
response_model_exclude={fields} |
Excluir poucos campos | Nível de campo |
response_model_include={fields} |
Contém apenas poucos campos | Nível de campo |
Field(exclude=True) |
Excluir um campo específico | Nível de definição de campo |
5. Modelos de Múltipla Resposta e Técnicas Avançadas
(1) Respostas Diferentes para o Mesmo Endpoint
(1) ▶ Exemplo: Modelo de Resposta Union
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Union
class ProductFound(BaseModel):
id: int
name: str
price: float
class ProductNotFound(BaseModel):
error: str
product_id: int
app = FastAPI()
@app.get("/products/{id}", response_model=Union[ProductFound, ProductNotFound])
async def search_product(id: int):
if id == 1:
return ProductFound(id=1, name="Widget", price=9.99)
return ProductNotFound(error="Not found", product_id=id)
Saída:
# Função definida com sucesso
(2) ▶ Exemplo: Modelo de Resposta de Lista
from fastapi import FastAPI
from pydantic import BaseModel
class PriceEntry(BaseModel):
product_id: int
price: float
recorded_at: str
app = FastAPI()
@app.get("/products/{id}/prices", response_model=list[PriceEntry])
async def get_price_history(id: int):
return [
{"product_id": id, "price": 29.99, "recorded_at": "2026-01-01"},
{"product_id": id, "price": 24.99, "recorded_at": "2026-02-01"},
{"product_id": id, "price": 34.99, "recorded_at": "2026-03-01"},
]
Saída:
# Função definida com sucesso
(2) exclude_unset e exclude_none
(3) ▶ Exemplo: exclude_unset retorna apenas campos com valores
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
class ProductUpdate(BaseModel):
name: Optional[str] = None
category: Optional[str] = None
price: Optional[float] = None
app = FastAPI()
@app.get(
"/products/{id}",
response_model=ProductUpdate,
response_model_exclude_unset=True,
)
async def get_product_partial(id: int):
# Apenas name foi definido, category e price permanecem None
return ProductUpdate(name="Updated Widget")
Saída:
INFO: 127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK
Saída (inclui apenas campos com valores):
{"name": "Updated Widget"}
| Opção | Efeito | Casos de Uso |
|---|---|---|
response_model_exclude_unset=True |
Excluir campos que não foram definidos | Resposta PATCH: retorna apenas os campos atualizados |
response_model_exclude_none=True |
Excluir campos com valor None | Simplifica a resposta removendo valores nulos |
response_model_exclude_defaults=True |
Excluir campos que usam valores padrão | Remover valores padrão redundantes |
response_model_by_alias=True |
Exibir campos usando aliases | O front end requer nomenclatura camelCase |
(4) ▶ Exemplo: response_model_by_alias
from fastapi import FastAPI
from pydantic import BaseModel, Field, ConfigDict
class ProductResponse(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id: int
product_name: str = Field(alias="productName")
unit_price: float = Field(alias="unitPrice", description="Price in USD")
app = FastAPI()
@app.get("/products/{id}", response_model=ProductResponse, response_model_by_alias=True)
async def get_product(id: int):
return {"id": id, "productName": "Widget", "unitPrice": 9.99}
Saída:
INFO: 127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK
Saída (usando aliases para nomes de campos):
{"id": 1, "productName": "Widget", "unitPrice": 9.99}
❓ Perguntas Frequentes
response_model e a anotação de tipo de retorno?response_model filtra campos não declarados e valida a saída, enquanto a anotação de tipo de retorno afeta apenas a geração de documentação OpenAPI e não filtra campos. Sempre use response_model.exclude_unset é mais útil?{"nested_model": {"secret_field"}}.response_model=list[ItemModel], e o FastAPI filtrará automaticamente cada elemento na lista de acordo com o modelo.response_model afeta o desempenho?orjson para otimizar a serialização.📖 Resumo
response_modelfiltra automaticamente campos não declarados para evitar vazamentos de dados sensíveis em nível de schemaresponse_model_exclude/response_model_includefornece controle refinado em nível de campo sem a necessidade de criar um novo modelo- O modelo de resposta
Unionsuporta retornar estruturas diferentes do mesmo endpoint; vejalist[Model]para respostas de lista response_model_exclude_unsetretorna apenas campos com valores atribuídos; adequado para cenários PATCHresponse_model_by_alias=Trueusa aliases para respostas para atender às convenções de nomenclatura camelCase do front end
📝 Exercícios
- Problema Básico (Dificuldade ⭐): Crie um modelo
ProductPublic(id, name, price), e useresponse_modelpara garantir que o endpoint retorne apenas esses três campos—mesmo que retorne um dict contendocost_price, nenhuma informação é vazada. Dica:@app.get(..., response_model=ProductPublic) - Exercício Avançado (Dificuldade ⭐⭐): Crie dois endpoints para o PriceTracker—um endpoint público que oculta
wholesale_price, e um endpoint admin que exibe todos os campos. Implemente isso usandoresponse_model_exclude. Dica:response_model_exclude={"wholesale_price"} - Desafio (Dificuldade ⭐⭐⭐): Crie um modelo
ProductDetailcom campos opcionais (como description, image_url, etc.), e useresponse_model_exclude_unset=Truepara garantir que o endpoint retorne apenas os campos fornecidos pelo cliente, com valores vazos omitidos do JSON. Dica:Optional[str] = None+response_model_exclude_unset=True
---|



