Pular para o conteúdo

    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:

    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())
    
    Saída
    {'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:

    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())
    
    Saída
    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):

    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())
    
    Saída
    {'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:

    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())
    
    Saída
    [{'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:

    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)
    
    Saída
    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.

    ModeloQuem usaO que tem
    UsuarioEntradaCorpo do POSTOs campos que o cliente envia, inclusive a senha
    UsuarioSaidaresponse_modelO que o cliente pode ver, sem a senha
    Registro do bancoSó o servidorTudo, 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.