Capítulo 6, Básico
Corpo da requisição e POST com Pydantic
O corpo é o dado que o cliente envia para criar ou alterar algo. Com um modelo do Pydantic, o FastAPI lê o JSON, valida cada campo e entrega à sua função um objeto pronto.
O corpo da requisição
Em um cadastro, o formulário manda nome, e-mail e senha. Esses dados vão no corpo da requisição, em JSON. O verbo para criar um recurso é o POST, e, ao contrário do GET, ele muda algo no servidor. Para descrever o corpo, você declara uma classe que herda de BaseModel:
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class Produto(BaseModel):
nome: str
preco: float
@app.post("/produtos")
def criar_produto(produto: Produto):
return {"mensagem": "Produto criado", "dados": produto}
cliente = TestClient(app)
resposta = cliente.post("/produtos", json={"nome": "Caneta", "preco": 3.5})
print(resposta.status_code, resposta.json())
200 {'mensagem': 'Produto criado', 'dados': {'nome': 'Caneta', 'preco': 3.5}}
O parâmetro produto: Produto diz ao FastAPI: "o corpo da requisição é um JSON com esta forma". Dentro da função, produto é um objeto, e você acessa produto.nome e produto.preco.
O que acontece quando o dado está errado
O Pydantic confere cada campo e responde com um 422 que aponta o problema. Os casos mais comuns são campo faltando, tipo errado e JSON malformado:
faltando = cliente.post("/produtos", json={"nome": "Caneta"})
print(faltando.status_code, faltando.json()["detail"][0]["loc"], faltando.json()["detail"][0]["type"])
tipo_errado = cliente.post("/produtos", json={"nome": "Caneta", "preco": "abc"})
print(tipo_errado.json()["detail"][0]["msg"])
quebrado = cliente.post("/produtos", content="{nome:", headers={"content-type": "application/json"})
print(quebrado.status_code, quebrado.json()["detail"][0]["type"])
422 ['body', 'preco'] missing
Input should be a valid number, unable to parse string as a number
422 json_invalid
O loc começa com body, que mostra que o problema está no corpo, seguido do nome do campo.
O Pydantic converte, quando é seguro
Por padrão, o Pydantic aceita valores que podem ser convertidos sem perda. Um preço enviado como o texto "19.9" vira o decimal 19.9. Isso é conveniente para formulários, mas, quando você não quer a conversão, existem tipos estritos:
resposta = cliente.post("/produtos", json={"nome": "Lápis", "preco": "19.9"})
print(resposta.json()["dados"])
{'nome': 'Lápis', 'preco': 19.9}
Um erro de entendimento muito comum
Você pode escrever uma rota POST com parâmetros simples, como def criar(nome: str, idade: int). Funciona, mas esses parâmetros não vão no corpo: como são tipos simples, o FastAPI os trata como parâmetros de consulta. Só um modelo do Pydantic (ou o Body) vai no corpo:
@app.post("/pessoas")
def criar_pessoa(nome: str, idade: int):
return {"nome": nome, "idade": idade}
print(cliente.post("/pessoas", params={"nome": "Ana", "idade": 30}).status_code)
resposta = cliente.post("/pessoas", json={"nome": "Ana", "idade": 30})
print(resposta.status_code, resposta.json()["detail"][0]["loc"])
200
422 ['query', 'nome']
O JSON no corpo foi ignorado, e o FastAPI reclamou que faltavam nome e idade na consulta. Dados de criação devem sempre ir em um modelo.
Dicionário ou modelo?
Receber um dict aceita qualquer coisa, sem estrutura nem validação, e você escreve cada checagem à mão. O modelo declara a estrutura uma vez, e ganha validação, conversão e documentação:
dict | Modelo Pydantic | |
|---|---|---|
| Campos | Livres | Declarados |
| Validação | Manual | Automática |
| Documentação | Genérica | Descreve cada campo |
| Autocompletar no editor | Não | Sim |
Usando o modelo como objeto
O modelo é uma classe comum, com métodos úteis para converter de e para dicionário e JSON:
produto = Produto(nome="Caderno", preco=12)
print(produto)
print(produto.model_dump())
print(produto.model_dump_json())
print(Produto.model_validate({"nome": "Régua", "preco": 4.5}))
nome='Caderno' preco=12.0
{'nome': 'Caderno', 'preco': 12.0}
{"nome":"Caderno","preco":12.0}
nome='Régua' preco=4.5
O preco=12 (inteiro) virou o decimal 12.0, porque o campo é float.
Exercício 1
Cadastrar um usuário
Crie POST /usuarios que receba um modelo Usuario com nome (texto) e idade (inteiro) e devolva {"mensagem": "Usuário criado", "nome": ..., "idade": ...}. Confira um caso válido e um com idade em texto.