Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions sqlmodel/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,16 @@ def sqlmodel_init(*, self: "SQLModel", data: Dict[str, Any]) -> None:
self_instance=self,
)
else:
raw_self = self.model_copy()
pydantic_validated_model = self.__pydantic_validator__.validate_python(
data,
self_instance=raw_self,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was raw_self necessary here?

Copy link
Author

@monchin monchin Nov 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was raw_self necessary here?

self.__pydantic_validator__.validate_python(
                data,
                self_instance=raw_self,
            )

would update self, and something would go wrong on sqlmodel_table_construct for the changed self. Maybe that's the reason that the original code dosen't validate when table=True.

Actually, we need to get the pydantic-validated dict, and use this dict to update the original dict, and then construct tables by the updated-original dict.

Anyway, you can try self_instance=self, and some unittests would fail.

Copy link

@spandan-sharma spandan-sharma Sep 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should also do:

data = raw_self.model_dump()
self.__dict__ |= raw_self.__dict__

after validate_python is called on raw_self.

Otherwise you'll likely have broken model-validator behaviour, double default-factory calls etc. I explain all this here including why you need model_copy (or maybe even __dict__.copy() would do) - see Full Solution section at the bottom to skip the problem description etc.

)
pydantic_dict = pydantic_validated_model.model_dump()
for k in pydantic_dict.keys():
if k not in data.keys():
continue
data[k] = pydantic_dict[k]
sqlmodel_table_construct(
self_instance=self,
values=data,
Expand Down
22 changes: 1 addition & 21 deletions tests/test_instance_no_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,7 @@

import pytest
from pydantic import ValidationError
from sqlmodel import Field, Session, SQLModel, create_engine, select


def test_allow_instantiation_without_arguments(clear_sqlmodel):
class Item(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
description: Optional[str] = None

engine = create_engine("sqlite:///:memory:")
SQLModel.metadata.create_all(engine)
with Session(engine) as db:
item = Item()
item.name = "Rick"
db.add(item)
db.commit()
statement = select(Item)
result = db.exec(statement).all()
assert len(result) == 1
assert isinstance(item.id, int)
SQLModel.metadata.clear()
from sqlmodel import Field, SQLModel


def test_not_allow_instantiation_without_arguments_if_not_table():
Expand Down
33 changes: 32 additions & 1 deletion tests/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest
from pydantic.error_wrappers import ValidationError
from sqlmodel import SQLModel
from sqlmodel import Field, SQLModel

from .conftest import needs_pydanticv1, needs_pydanticv2

Expand Down Expand Up @@ -63,3 +63,34 @@ def reject_none(cls, v):

with pytest.raises(ValidationError):
Hero.model_validate({"name": None, "age": 25})


@needs_pydanticv2
def test_validation_with_table_true():
"""Test validation with table=True."""
from pydantic import field_validator

class Hero(SQLModel, table=True):
name: Optional[str] = Field(default=None, primary_key=True)
secret_name: Optional[str] = None
age: Optional[int] = None

@field_validator("age", mode="after")
@classmethod
def double_age(cls, v):
if v is not None:
return v * 2
return v

Hero(name="Deadpond", age=25)
Hero.model_validate({"name": "Deadpond", "age": 25})
with pytest.raises(ValidationError):
Hero(name="Deadpond", secret_name="Dive Wilson", age="test")
with pytest.raises(ValidationError):
Hero.model_validate({"name": "Deadpond", "age": "test"})

double_age_hero = Hero(name="Deadpond", age=25)
assert double_age_hero.age == 50

double_age_hero = Hero.model_validate({"name": "Deadpond", "age": 25})
assert double_age_hero.age == 50