From 314c7558eca0dff017aafb10af21625b20972c1a Mon Sep 17 00:00:00 2001 From: Tesla2000 Date: Sun, 19 Apr 2026 17:00:55 +0200 Subject: [PATCH] Fix issue with relationship dict Added handling to use_obj dictionary --- sqlmodel/_compat.py | 6 ++- tests/test_relationship_dict.py | 67 +++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 tests/test_relationship_dict.py diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index a220b193f1..3724c30a1a 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -320,7 +320,11 @@ def sqlmodel_validate( # Get and set any relationship objects if is_table_model_class(cls): for key in new_obj.__sqlmodel_relationships__: - value = getattr(use_obj, key, Undefined) + value = ( + use_obj.get(key, Undefined) + if isinstance(use_obj, dict) + else getattr(use_obj, key, Undefined) + ) if value is not Undefined: setattr(new_obj, key, value) return new_obj diff --git a/tests/test_relationship_dict.py b/tests/test_relationship_dict.py new file mode 100644 index 0000000000..35e81f617f --- /dev/null +++ b/tests/test_relationship_dict.py @@ -0,0 +1,67 @@ +""" +Regression test for: model_validate with a dict containing relationship keys +silently drops the relationship data. + +Root cause: sqlmodel_validate in _compat.py uses getattr(use_obj, key, Undefined) +to extract relationship values. When use_obj is a dict, getattr finds no attribute +named e.g. "heroes", returning Undefined, so relationships are never set. + +Fix: use dict.get() when use_obj is a dict instance. +""" + +from sqlalchemy.orm import selectinload +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +def test_model_validate_dict_populates_relationships(): + """Relationships passed as dict keys to model_validate should be set.""" + + class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + heroes: list["Hero"] = Relationship(back_populates="team") + + class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Team | None = Relationship(back_populates="heroes") + + heroes = [Hero(name="Deadpond"), Hero(name="Spider-Boy")] + team = Team.model_validate({"name": "Z-Force", "heroes": heroes}) + + assert len(team.heroes) == 2, ( + "model_validate with a dict should populate relationship fields" + ) + + +def test_model_validate_dict_relationships_persisted(): + """Relationships set via model_validate dict should be saved and loadable.""" + + class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + heroes: list["Hero"] = Relationship(back_populates="team") + + class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Team | None = Relationship(back_populates="heroes") + + engine = create_engine("sqlite://") + SQLModel.metadata.create_all(engine) + + heroes = [Hero(name="Deadpond"), Hero(name="Spider-Boy")] + team = Team.model_validate({"name": "Z-Force", "heroes": heroes}) + + with Session(engine) as session: + session.add(team) + session.commit() + + with Session(engine) as session: + loaded = session.exec( + select(Team).options(selectinload(Team.heroes)) # type: ignore[arg-type] + ).first() + assert loaded is not None + assert len(loaded.heroes) == 2