Skip to content

    Chapter 6, Basic

    Request body and POST with Pydantic

    The body is the data that the client sends to create or alter something. With a Pydantic model, FastAPI reads the JSON, validates each field, and delivers a ready object to your function.

    The request body

    In a registration, the form sends name, email, and password. This data goes in the body of the request, in JSON. The verb to create a resource is the POST, and, unlike the GET, it changes something on the server. To describe the body, you declare a class that inherits from BaseModel:

    basico/cap06_corpo_pydantic.pylinhas 10 a 29
    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())
    
    Output
    200 {'mensagem': 'Produto criado', 'dados': {'nome': 'Caneta', 'preco': 3.5}}
    

    The parameter produto: Produto tells FastAPI: "the body of the request is a JSON with this shape". Inside the function, produto is an object, and you access produto.nome and produto.preco.

    What happens when the data is wrong

    Pydantic checks each field and responds with a 422 that points out the problem. The most common cases are missing field, wrong type, and malformed JSON:

    basico/cap06_corpo_pydantic.pylinhas 34 a 41
    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"])
    
    Output
    422 ['body', 'preco'] missing
    Input should be a valid number, unable to parse string as a number
    422 json_invalid
    

    The loc starts with body, which shows that the problem is in the body, followed by the name of the field.

    Pydantic converts when it's safe

    By default, Pydantic accepts values that can be converted without loss. A price sent as the text "19.9" becomes the decimal 19.9. This is convenient for forms, but when you don't want the conversion, there are strict types:

    basico/cap06_corpo_pydantic.pylinhas 46 a 47
    resposta = cliente.post("/produtos", json={"nome": "Lápis", "preco": "19.9"})
    print(resposta.json()["dados"])
    
    Output
    {'nome': 'Lápis', 'preco': 19.9}
    

    A very common misunderstanding

    You can write a route POST with simple parameters, like def criar(nome: str, idade: int). It works, but these parameters do not go in the body: since they are simple types, FastAPI treats them as query parameters. Only a Pydantic model (or the Body) goes in the body:

    basico/cap06_corpo_pydantic.pylinhas 52 a 59
    @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"])
    
    Output
    200
    422 ['query', 'nome']
    

    The JSON in the body was ignored, and FastAPI complained that it was missing nome and idade in the parameters. Creation data must always go in a model.

    Dictionary or model?

    Receiving a dict accepts anything, without structure or validation, and you write each check by hand. The model declares the structure once, and gains validation, conversion, and documentation:

    dictPydantic Model
    FieldsFreeDeclared
    ValidationManualAutomatic
    DocumentationGenericDescribes each field
    Autocomplete in the editorNoYes

    Using the model as an object

    The model is a regular class, with useful methods to convert to and from dictionary and JSON:

    basico/cap06_corpo_pydantic.pylinhas 64 a 68
    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}))
    
    Output
    nome='Caderno' preco=12.0
    {'nome': 'Caderno', 'preco': 12.0}
    {"nome":"Caderno","preco":12.0}
    nome='Régua' preco=4.5
    

    The preco=12 (integer) became the decimal 12.0, because the field is float.

    Exercise 1

    Register a user

    Create POST /usuarios that receives a model Usuario with nome (text) and idade (integer) and returns {"mensagem": "Usuário criado", "nome": ..., "idade": ...}. Check a valid case and one with age in text.