diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index a220b193f1..e1075387e7 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -320,7 +320,13 @@ 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) + # use_obj can be a dict (the input obj, or the merged obj when + # update is passed), so read relationships accordingly instead of + # assuming attribute access. + if isinstance(use_obj, dict): + value = use_obj.get(key, Undefined) + else: + value = getattr(use_obj, key, Undefined) if value is not Undefined: setattr(new_obj, key, value) return new_obj diff --git a/tests/test_validation.py b/tests/test_validation.py index 47fbca87c2..cc8f27c74c 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -29,3 +29,28 @@ def reject_none(cls, v): with pytest.raises(ValidationError): Hero.model_validate({"name": None, "age": 25}) + + +def test_validate_dict_sets_relationship(clear_sqlmodel): + """A relationship passed inside the dict given to model_validate must be + set, consistent with the constructor and with model_validate(object).""" + + from sqlmodel import Field, Relationship + + 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") + + team = Team(name="Avengers") + + # constructor already works; model_validate must match it + assert Hero(name="IronMan", team=team).team is team + assert Hero.model_validate({"name": "Thor", "team": team}).team is team + assert Hero.model_validate({"name": "Hulk"}, update={"team": team}).team is team