Chapter 10, Basic
Response models
Not everything that exists in the backend can reach the client. The `response_model` is the filter that decides what goes out, and it's the simplest defense against leaking a password.
The problem
A user record in the database has name, age, and password. The client should never see the password. Instead of remembering to delete it in each route, you declare the output format, and FastAPI applies the filter for you. I separate the models by role, reusing common fields through inheritance:
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}
The function returned the record whole, with the password, but only id, nome and idade reached the client. FastAPI passed the return through the UsuarioSaida and discarded the rest.
Create without ever returning the password
The input requires the password, and the output never contains it. This also applies to the POST: never return the password you just received, neither in plain text nor as a 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
The return annotation also serves
Instead of the parameter response_model, you can annotate the return type of the function, and FastAPI uses it the same way (including for filtering):
@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}
I use annotation when I can, because it also helps the type checker and the editor.
Lists and omitted values
For a list, the model goes inside list[...]. Optional fields that are worth None can be omitted from the response with 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'}]
FastAPI also validates the output
If the function returns something that does not match the model (a idade that is not a number, a missing field), FastAPI does not send garbage to the client: it raises a server error. The TestClient rethrows the exception by default, and I turn it off here to see the 500 that a real client would receive:
@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
A 500 here is a bug of yours, and not of the client, and it is good that it appears in tests instead of producing wrong data silently.
| Model | Who uses | What it has |
|---|---|---|
UsuarioEntrada | Body of POST | The fields that the client sends, including the password |
UsuarioSaida | response_model | What the client can see, without the password |
| Database record | Only the server | Everything, including the password (stored as a hash, chapter 19) |
Exercise 1
Hide the cost
A product has nome, preco and custo (the value that the store paid, which the client cannot see). Create GET /produtos/{produto_id} with an output model that shows only name and price.