Capítulo 10, Básico
Modelos de resposta
Nem tudo o que existe no backend pode chegar ao cliente. O `response_model` é o filtro que decide o que sai, e é a defesa mais simples contra vazar uma senha.
O problema
Um registro de usuário no banco tem nome, idade e senha. O cliente nunca deve ver a senha. Em vez de lembrar de apagá-la em cada rota, você declara o formato da saída, e o FastAPI aplica o filtro por você. Eu separo os modelos por papel, reaproveitando os campos comuns por herança:
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class UsuarioBase(BaseModel):
nome: str
idade: int
class UsuarioEntrada(UsuarioBase):
senha: str
class UsuarioSaida(UsuarioBase):
id: int
banco: dict[int, dict] = {
1: {"id": 1, "nome": "Mohit", "idade": 24, "senha": "segredo-do-banco"},
}
@app.get("/usuarios/{usuario_id}", response_model=UsuarioSaida)
def obter(usuario_id: int):
if usuario_id not in banco:
raise HTTPException(status_code=404, detail="Usuário não encontrado")
return banco[usuario_id]
cliente = TestClient(app)
print(cliente.get("/usuarios/1").json())
{'nome': 'Mohit', 'idade': 24, 'id': 1}
A função devolveu o registro inteiro, com a senha, mas só id, nome e idade chegaram ao cliente. O FastAPI passou o retorno pelo UsuarioSaida e descartou o resto.
Criar sem nunca devolver a senha
A entrada exige a senha, e a saída nunca a contém. Isso vale também para o POST: nunca devolva a senha que acabou de receber, nem em texto puro, nem como hash:
@app.post("/usuarios", response_model=UsuarioSaida, status_code=201)
def criar(usuario: UsuarioEntrada):
novo_id = max(banco, default=0) + 1
banco[novo_id] = {"id": novo_id, **usuario.model_dump()}
return banco[novo_id]
resposta = cliente.post("/usuarios", json={"nome": "Rohit", "idade": 30, "senha": "abc12345"})
print(resposta.status_code, resposta.json())
print("senha" in resposta.json())
201 {'nome': 'Rohit', 'idade': 30, 'id': 2}
False
A anotação de retorno também serve
Em vez do parâmetro response_model, você pode anotar o tipo de retorno da função, e o FastAPI o usa do mesmo jeito (inclusive para filtrar):
@app.get("/perfil/{usuario_id}")
def perfil(usuario_id: int) -> UsuarioSaida:
return banco[usuario_id]
print(cliente.get("/perfil/1").json())
{'nome': 'Mohit', 'idade': 24, 'id': 1}
Eu uso a anotação quando posso, porque ela ajuda também o verificador de tipos e o editor.
Listas e valores omitidos
Para uma lista, o modelo vai dentro de list[...]. Campos opcionais que valem None podem ser omitidos da resposta com response_model_exclude_none:
class Resumo(BaseModel):
nome: str
apelido: str | None = None
@app.get("/resumos", response_model=list[Resumo], response_model_exclude_none=True)
def resumos():
return [{"nome": "Ana", "apelido": "Aninha"}, {"nome": "Bia"}]
print(cliente.get("/resumos").json())
[{'nome': 'Ana', 'apelido': 'Aninha'}, {'nome': 'Bia'}]
O FastAPI valida também a saída
Se a função devolver algo que não bate com o modelo (um idade que não é número, um campo faltando), o FastAPI não envia lixo ao cliente: ele levanta um erro de servidor. O TestClient relança a exceção por padrão, e eu a desligo aqui para ver o 500 que um cliente real receberia:
@app.get("/quebrado", response_model=UsuarioSaida)
def quebrado():
return {"id": 1, "nome": "Ana", "idade": "não sei"}
sem_relancar = TestClient(app, raise_server_exceptions=False)
resposta = sem_relancar.get("/quebrado")
print(resposta.status_code, resposta.text)
500 Internal Server Error
Um 500 aqui é um bug seu, e não do cliente, e é bom que ele apareça nos testes em vez de produzir dados errados em silêncio.
| Modelo | Quem usa | O que tem |
|---|---|---|
UsuarioEntrada | Corpo do POST | Os campos que o cliente envia, inclusive a senha |
UsuarioSaida | response_model | O que o cliente pode ver, sem a senha |
| Registro do banco | Só o servidor | Tudo, inclusive a senha (guardada como hash, capítulo 19) |
Exercício 1
Esconder o custo
Um produto tem nome, preco e custo (o valor que a loja pagou, que o cliente não pode ver). Crie GET /produtos/{produto_id} com um modelo de saída que mostre só nome e preço.