Saltar al contenido

Body - Actualizaciones

Actualizar reemplazando con PUT

Para actualizar un item puedes usar la operación HTTP PUT.

Puedes usar jsonable_encoder para convertir los datos de entrada a datos que pueden almacenarse como JSON (por ejemplo, con una base de datos NoSQL). Por ejemplo, convirtiendo datetime a str.

from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str | None = None
    description: str | None = None
    price: float | None = None
    tax: float = 10.5
    tags: list[str] = []


items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
    "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}


@app.get("/items/{item_id}", response_model=Item)
async def read_item(item_id: str):
    return items[item_id]


@app.put("/items/{item_id}", response_model=Item)
async def update_item(item_id: str, item: Item):
    update_item_encoded = jsonable_encoder(item)
    items[item_id] = update_item_encoded
    return update_item_encoded

PUT se usa para recibir datos que deben reemplazar los datos existentes.

Advertencia sobre reemplazar

Eso significa que si quieres actualizar el item bar usando PUT con un body que contenga:

{
    "name": "Barz",
    "price": 3,
    "description": None,
}

porque no incluye el atributo ya almacenado "tax": 20.2, el modelo de entrada tomaría el valor por defecto de "tax": 10.5.

Y los datos se guardarían con ese "nuevo" tax de 10.5.

Actualizaciones parciales con PATCH

También puedes usar la operación HTTP PATCH para actualizar datos parcialmente.

Esto significa que puedes enviar solo los datos que quieres actualizar, dejando el resto intacto.

Nota

PATCH es menos comúnmente usado y conocido que PUT.

Y muchos equipos usan solo PUT, incluso para actualizaciones parciales.

Eres libre de usarlos como quieras, FastAPI no impone ninguna restricción.

Pero esta guía te muestra, más o menos, cómo se pretende usarlos.

Usar el parámetro exclude_unset de Pydantic

Si quieres recibir actualizaciones parciales, es muy útil usar el parámetro exclude_unset en el .model_dump() del modelo de Pydantic.

Como item.model_dump(exclude_unset=True).

Eso generaría un dict con solo los datos que se establecieron al crear el modelo item, excluyendo los valores por defecto.

Luego puedes usar esto para generar un dict con solo los datos que se establecieron (enviados en la petición), omitiendo los valores por defecto:

from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str | None = None
    description: str | None = None
    price: float | None = None
    tax: float = 10.5
    tags: list[str] = []


items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
    "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}


@app.get("/items/{item_id}", response_model=Item)
async def read_item(item_id: str):
    return items[item_id]


@app.patch("/items/{item_id}")
async def update_item(item_id: str, item: Item) -> Item:
    stored_item_data = items[item_id]
    stored_item_model = Item(**stored_item_data)
    update_data = item.model_dump(exclude_unset=True)
    updated_item = stored_item_model.model_copy(update=update_data)
    items[item_id] = jsonable_encoder(updated_item)
    return updated_item

Usar el parámetro update de Pydantic

Ahora, puedes crear una copia del modelo existente usando .model_copy(), y pasar el parámetro update con un dict conteniendo los datos a actualizar.

Como stored_item_model.model_copy(update=update_data):

from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str | None = None
    description: str | None = None
    price: float | None = None
    tax: float = 10.5
    tags: list[str] = []


items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
    "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}


@app.get("/items/{item_id}", response_model=Item)
async def read_item(item_id: str):
    return items[item_id]


@app.patch("/items/{item_id}")
async def update_item(item_id: str, item: Item) -> Item:
    stored_item_data = items[item_id]
    stored_item_model = Item(**stored_item_data)
    update_data = item.model_dump(exclude_unset=True)
    updated_item = stored_item_model.model_copy(update=update_data)
    items[item_id] = jsonable_encoder(updated_item)
    return updated_item

Resumen de actualizaciones parciales

En resumen, para aplicar actualizaciones parciales harías:

  • (Opcionalmente) usar PATCH en lugar de PUT.
  • Recuperar los datos almacenados.
  • Poner esos datos en un modelo Pydantic.
  • Generate a dict without default values from the input model (using exclude_unset).
    • De esta manera puedes actualizar solo los valores realmente establecidos por el usuario, en lugar de sobrescribir valores ya almacenados con valores por defecto en tu modelo.
  • Crear una copia del modelo almacenado, actualizando sus atributos con las actualizaciones parciales recibidas (usando el parámetro update).
  • Convert the copied model to something that can be stored in your DB (for example, using the jsonable_encoder).
    • Esto es comparable a usar el método .model_dump() del modelo de nuevo, pero se asegura (y convierte) los valores a tipos de datos que pueden convertirse a JSON, por ejemplo, datetime a str.
  • Guardar los datos en tu DB.
  • Devolver el modelo actualizado.
from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str | None = None
    description: str | None = None
    price: float | None = None
    tax: float = 10.5
    tags: list[str] = []


items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
    "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}


@app.get("/items/{item_id}", response_model=Item)
async def read_item(item_id: str):
    return items[item_id]


@app.patch("/items/{item_id}")
async def update_item(item_id: str, item: Item) -> Item:
    stored_item_data = items[item_id]
    stored_item_model = Item(**stored_item_data)
    update_data = item.model_dump(exclude_unset=True)
    updated_item = stored_item_model.model_copy(update=update_data)
    items[item_id] = jsonable_encoder(updated_item)
    return updated_item

Consejo

En realidad puedes usar esta misma técnica con una operación HTTP PUT.

Pero el ejemplo aquí usa PATCH porque fue creado para estos casos de uso.

Nota

Nota que el modelo de entrada sigue siendo validado.

Así que, si quieres recibir actualizaciones parciales que pueden omitir todos los atributos, necesitas tener un modelo con todos los atributos marcados como opcionales (con valores por defecto o None).

Para distinguir entre los modelos con todos los valores opcionales para actualizaciones y los modelos con valores requeridos para creación, puedes usar las ideas descritas en Extra Models.