Skip to content

sqlmodel_update() overwrites all fields from BaseModel input, silently destroying data in PATCH endpoints #1835

@mahdirajaee

Description

@mahdirajaee

Description

When sqlmodel_update() receives a BaseModel instance, it iterates over every field and calls setattr for each one — including fields the caller never set. There is no exclude_unset parameter and no awareness of __pydantic_fields_set__.

Reproduction

class HeroUpdate(SQLModel):
    name: str | None = None
    age: int | None = None

@app.patch("/heroes/{hero_id}")
def update_hero(hero_id: int, hero_update: HeroUpdate, session: Session = Depends(get_session)):
    db_hero = session.get(Hero, hero_id)
    db_hero.sqlmodel_update(hero_update)  # Overwrites age=None even if client only sent name
    session.add(db_hero)
    session.commit()

Client sends {"name": "New Name"}. hero_update.age defaults to None. The method overwrites the hero's existing age with None, silently destroying data.

Current Workaround

db_hero.sqlmodel_update(hero_update.model_dump(exclude_unset=True))

This works but is non-obvious. The method's type signature explicitly accepts BaseModel, implying it handles this case correctly.

Proposed Fix

Add an exclude_unset: bool = False parameter to sqlmodel_update(). When True and obj is a BaseModel, filter to only obj.__pydantic_fields_set__ before applying:

def sqlmodel_update(self, obj, *, exclude_unset: bool = False):
    if isinstance(obj, BaseModel):
        if exclude_unset:
            data = obj.model_dump(exclude_unset=True)
        else:
            data = {field: getattr(obj, field) for field in get_model_fields(obj)}
    # ...

This is the single most common FastAPI + SQLModel pattern and the silent data corruption is a significant footgun.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions