Skip to content

    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:

    basico/cap10_modelos_resposta.pylinhas 10 a 43
    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())
    
    Output
    {'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:

    basico/cap10_modelos_resposta.pylinhas 48 a 57
    @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())
    
    Output
    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):

    basico/cap10_modelos_resposta.pylinhas 62 a 67
    @app.get("/perfil/{usuario_id}")
    def perfil(usuario_id: int) -> UsuarioSaida:
        return banco[usuario_id]
    
    
    print(cliente.get("/perfil/1").json())
    
    Output
    {'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:

    basico/cap10_modelos_resposta.pylinhas 72 a 82
    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())
    
    Output
    [{'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:

    basico/cap10_modelos_resposta.pylinhas 87 a 94
    @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)
    
    Output
    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.

    ModelWho usesWhat it has
    UsuarioEntradaBody of POSTThe fields that the client sends, including the password
    UsuarioSaidaresponse_modelWhat the client can see, without the password
    Database recordOnly the serverEverything, 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.