Skip to content

Why does a SQLModel class with table=True not validate data ? #453

Closed

Description

@deajan

First Check

  • I added a very descriptive title to this issue.
  • I used the GitHub search to find a similar issue and didn't find it.
  • I searched the SQLModel documentation, with the integrated search.
  • I already searched in Google "How to X in SQLModel" and didn't find any information.
  • I already read and followed all the tutorial in the docs and didn't find an answer.
  • I already checked if it is not related to SQLModel but to Pydantic.
  • I already checked if it is not related to SQLModel but to SQLAlchemy.

Commit to Help

  • I commit to help with one of those options 👆

Example Code

from pydantic import BaseModel
from sqlmodel import SQLModel, Field


class BaseModelTest(BaseModel):
    id: int
    some_bool: bool
    desc: str

class SQLModelTest(SQLModel, table=True):
    id: int = Field(default=None, primary_key=True)
    some_bool: bool
    desc: str


# This will work as expected
sqlmodel_instance = SQLModelTest(id=3, some_bool=False, desc="This will work")
print(sqlmodel_instance)
# This will work as expected
pydantic_instance = BaseModelTest(id=3, some_bool=False, desc="This will work")
print(pydantic_instance)


# This will work but should not
sqlmodel_instance = SQLModelTest(id=3, some_bool="blob", desc=False)
print(sqlmodel_instance)
# This will not work as expected
pydantic_instance = BaseModelTest(id=3, some_bool="blob", desc=False)
print(pydantic_instance)

Description

When creating a SQLModel class instance, I thought that I'd have all the advantages of pydantic and SQLAlchemy classes toghether.
I was quite surprised when I realized that SQLModel classes created with table=True do not validate data as a pydantic class would do (see my example code).
Having read about all SQLModel documentation, I didn't find anywhere this is stated.
Especially in https://sqlmodel.tiangolo.com/tutorial/fastapi/multiple-models/ it would make sense to explain that a table=True won't do data validation as a pydantic class would do.

Maybe I did not understand the purpose of SQLModel enough, but this is quite disturbing for me.
I think it would be really nice to update the documentation regarding this particular point.

Operating System

Windows

Operating System Details

No response

SQLModel Version

0.0.8

Python Version

Python 3.10.7 x64

Additional Context

PS: Sorry, I'm comming here from Tortoise-ORM, and it's my first time using SQLModel, so I cannot guarantee that I will be quite helpful resolving issues here yet ;) I'll do my best.

Activity

  1. phi-friday commented on Sep 29, 2022

    @phi-friday
    Contributor

    To guess, when used as a table object, there is no verification process because it is used to map data that has already been verified in database.
    (I think because SQLModel use DeclarativeMeta when table=True. link)
    In particular, when calling many objects from database, there is no unnecessary verification process, so there is a performance advantage.
    (I haven't checked yet.)
    Of course, if the verification process is essential, SQLModel.validate method can be used.

    I've asked the same question before, but I'm just using SQLModel.validate method for now. I want to know why, too.

  2. deajan commented on Sep 29, 2022

    @deajan
    Author

    I do understand that too, reads from database don't need validation. But writes do. Nevertheless it's not really clear in the documentation.
    And since SQLModel is supposed to reduce code duplication, having a class for ORM and a class for Pydantic validation.... doesn't help deduplicate.

  3. phi-friday commented on Oct 1, 2022

    @phi-friday
    Contributor

    @deajan
    I don't know the answer to why, but I found a simple way to solve it.
    link

  4. 5p4k commented on Dec 8, 2023

    @5p4k

    I think this is answered here: #52 (comment)

    For future reference, I found this while looking for why model_validate_json did not properly validate for a SQLModel with table=True.

  5. realchandan commented on Jul 13, 2024

    @realchandan

    This feature/bug should be fully documented, as I (and many others) expect the SQL model to validate data. Only after doing a lot of digging to find out why one of my field_validator wasn't working did I come across this GitHub issue.

  6. mg3146 commented on Oct 28, 2024

    @mg3146

    I agree - this just totally threw me off. I expected it to be fully validating my table data. why bother inheriting from BaseModel at all if not.

    This feature/bug should be fully documented, as I (and many others) expect the SQL model to validate data. Only after doing a lot of digging to find out why one of my field_validator wasn't working did I come across this GitHub issue.

  7. RamiroFartoBello commented on Nov 20, 2024

    @RamiroFartoBello

    completely annoying bug. I've lost half a day trying to understand what was happening. I'll reconsider if SQLModel is actually the way to go. Thank you, guys!

  8. iloveitaly commented on Nov 26, 2024

    @iloveitaly

    This was also very surprising to me. Looks like this PR may fix the issue, although it's unclear if it's the right approach.

  9. fastkits commented on Feb 14, 2025

    @fastkits

    when update new version ?

  10. ml31415 commented on Mar 2, 2025

    @ml31415

    when update new version ?

    I don't think, that there will be a version fixing this. It rather looks like, that this oddity is considered a feature. Sadly.

  11. SDVoGo commented on Mar 15, 2025

    @SDVoGo

    That's not what I expected.
    I have been looking for a solution in the docs (in SQLmodel and Pydantic too) for 3 days now.
    Why would I expect an error from the DB rather than my backend application?

    I mean, I don't consider myself a senior so I guess whoever is working on it knows better than me but:
    If the intended behavior is waiting for the DB to raise an error. Why are my tests still committed?

    My problem was really stupid.

    class Formula(SQLModel, table=True):
        id: int | None = Field(default=None, primary_key=True)
        name: str
        formula: str

    name and formula are strings not None. I accidentally found that empty string was ok for both but I didn't like that.

    I can't use @model_validator(mode="after") because won't consider any validator.

  12. rainergo commented on Mar 16, 2025

    @rainergo

    The whole point for me of considering SQLModel was to avoid duplication required by the mapping from my data model (type checking with Pydantic) to the mapping of my database model (ORM). If this is not a bug, then SQLModel is totally useless.

  13. Samreay commented on Aug 14, 2025

    @Samreay

    I see I'm late to this party after many hours spent banging my head against a wall in my code base. The fascinating part is that the model validators are called with model_validate but not model_validate_json. In case its useful at all for disambiguation, here's what I had written up to post as the replication for a new issue before finding this one:

    from datetime import datetime as dt
    from datetime import timezone as tz
    from typing import Annotated, Self
    
    from pydantic import BeforeValidator
    from sqlmodel import Field, SQLModel
    
    IntDatetime = Annotated[
        dt,
        BeforeValidator(lambda x: dt.fromtimestamp(x / 1000, tz=tz.utc) if isinstance(x, int) else x),
    ]
    
    
    class ExampleWorks(SQLModel):
        time: IntDatetime = Field(primary_key=True)
    
    
    class ExampleBroken(SQLModel, table=True):
        time: IntDatetime = Field(primary_key=True)
    
    
    class NastyBase(SQLModel):
        @classmethod
        def model_validate_json(cls, json: str | bytes) -> Self:
            model = super().model_validate_json(json)
            return cls.model_validate(model)
    
    
    class ExampleBrokenFixed(NastyBase, table=True):
        time: IntDatetime = Field(primary_key=True)
    
    
    print("Without a table, validation works:")
    print(ExampleWorks(time=dt(2025, 8, 14, tzinfo=tz.utc)))
    print(ExampleWorks.model_validate({"time": 1755129600000}))
    print(ExampleWorks.model_validate_json('{"time": 1755129600000}'))
    
    print("\nMaking it a table breaks validation:")
    print(ExampleBroken(time=dt(2025, 8, 14, tzinfo=tz.utc)))
    print(ExampleBroken.model_validate({"time": 1755129600000}))
    print(ExampleBroken.model_validate_json('{"time": 1755129600000}'))
    
    print("\nForcing revalidation makes this slow but it technically works:")
    print(ExampleBrokenFixed(time=dt(2025, 8, 14, tzinfo=tz.utc)))
    print(ExampleBrokenFixed.model_validate({"time": 1755129600000}))
    print(ExampleBrokenFixed.model_validate_json('{"time": 1755129600000}'))

    Outputs:

    Without a table, validation works:
    time=datetime.datetime(2025, 8, 14, 0, 0, tzinfo=datetime.timezone.utc)
    time=datetime.datetime(2025, 8, 14, 0, 0, tzinfo=datetime.timezone.utc)
    time=datetime.datetime(2025, 8, 14, 0, 0, tzinfo=datetime.timezone.utc)
    
    Making it a table breaks validation:
    time=datetime.datetime(2025, 8, 14, 0, 0, tzinfo=datetime.timezone.utc)
    time=datetime.datetime(2025, 8, 14, 0, 0, tzinfo=datetime.timezone.utc)
    time=1755129600000
    
    Forcing revalidation makes this slow but it technically works:
    time=datetime.datetime(2025, 8, 14, 0, 0, tzinfo=datetime.timezone.utc)
    time=datetime.datetime(2025, 8, 14, 0, 0, tzinfo=datetime.timezone.utc)
    time=datetime.datetime(2025, 8, 14, 0, 0, tzinfo=datetime.timezone.utc)
    
  14. m-danya commented on Aug 14, 2025

    @m-danya

    Well, it was really unpleasant to discover... The whole point of the library, to combine pydantic and sqlalchemy, seems to have failed.

    This example prints <class 'str'>:

    from datetime import datetime
    from fastapi import FastAPI
    from sqlmodel import Field, SQLModel
    import uvicorn
    
    app = FastAPI()
    
    
    class FavoriteDate(SQLModel, table=True):
        id: int | None = Field(default=None, primary_key=True)
        date: datetime
    
    
    @app.post("/date")
    async def add_date(favorite_date: FavoriteDate):
        print(type(favorite_date.date))
    
    
    uvicorn.run(app, port=5000)
  15. spandan-sharma commented on Sep 2, 2025

    @spandan-sharma

    Pasting the TL;DR from here


    TL;DR

    You don't need multi-models. Here's a complete solution to address the problems:

    from contextlib import contextmanager
    from contextvars import ContextVar
    from typing import Self
    
    import sqlmodel as sm
    
    _ONGOING_MODEL_VALIDATE: ContextVar[bool] = ContextVar('_ONGOING_MODEL_VALIDATE', False)
    
    @contextmanager
    def set_ongoing_model_validate():
        token = _ONGOING_MODEL_VALIDATE.set(True)
        yield
        _ONGOING_MODEL_VALIDATE.reset(token)
    
    class OurBase(sm.SQLModel):
        model_config = {'validate_assignment': True}
    
        def __init__(self, **data):
            if self.model_config.get('table', False) and not _ONGOING_MODEL_VALIDATE.get():
                self_copy = self.model_copy()
                self.__pydantic_validator__.validate_python(data, self_instance=self_copy)
                data = self_copy.model_dump()
                self.__dict__ |= self_copy.__dict__
            super().__init__(**data)
    
        @classmethod
        def model_validate(cls: type[Self], *args, **kwargs) -> Self:
            with set_ongoing_model_validate():
                return super().model_validate(*args, **kwargs)
    

    Derive from this base (modify model_config etc if needed) and you should be fine. Given the age, I'm not hopeful for any official solution landing, so I'm just using this and moving on. To know why/how it works, read on.

  16. leogout commented on Nov 27, 2025

    @leogout

    Pasting the TL;DR from here

    TL;DR

    You don't need multi-models. Here's a complete solution to address the problems:
    [...]
    Derive from this base (modify model_config etc if needed) and you should be fine. Given the age, I'm not hopeful for any official solution landing, so I'm just using this and moving on. To know why/how it works, read on.

    To me, the whole point of using a library is to avoid digging in its protected attributes and private methods. Running undocumented code in an untested way based on a comment in a Github thread dosen't feel appealing.

    This is the first paragraph of the README :

    It combines SQLAlchemy and Pydantic and tries to simplify the code you write as much as possible, allowing you to reduce the code duplication to a minimum, but while getting the best developer experience possible.

    I surely don't understand all the pros and cons of this library as it is used by a lot of people, but my usecase, which I thought simple, isn't covered. Seeing the hundreds of reactions and the number of issues opened on this subject, I would say something isn't working as people expected. Maybe it should be made clear from the start that this usecase is not covered, and that people will need to write separate classes extending eachother to manage the validation and the orm separately. The README says it "combines" pydantics and SQLAlchemy but it is unclear to me how it combines them.

    In any case, thank you for your work, provided for free to me and others. I hope I'll find another usecase for it.

  17. MarleTangible commented on Jan 6, 2026

    @MarleTangible

    I just would like to make this absolutely clear.

    The library claims (statement) to combine pydantic and sqlalchemy models, but in its very basic use-case it doesn't validate the request against the model when used with FastAPI which defeats the purpose.

    Even the code from the simple api tutorial returns a 500 internal error instead of a 422 validation error. This is the very first endpoint users create with the library, so either the library description should be updated with corrected claims, or it should be made clear from get-go that defining a class with table=True is not validated due to these (listed) reasons.

    # great simple way of processing the Hero class
    @app.post("/heroes/")
    def create_hero(hero: Hero):
        with Session(engine) as session:
            session.add(hero)
            session.commit()
            session.refresh(hero)
            return hero

    Please don't get me wrong, the library is great in the fact that I don't need to deal with sqlalchemy types and simply create a pydantic model and interact with the database. But I still need a separate model to first validate the user input. The final list of models becomes something like below, see my comments please.

    # common fields
    class HeroBase(SQLModel):
        name: str = Field(index=True)
        secret_name: str
        age: int | None = Field(default=None, index=True)
    
    # database model
    class Hero(HeroBase, table=True):
        id: int | None = Field(default=None, primary_key=True)
    
    # model for post requests
    class HeroCreate(HeroBase):
        pass
    
    # model for responses
    # practically the same as Hero but reasons ...
    class HeroPublic(HeroBase):
        id: int
  18. shubhamyx commented on Apr 21, 2026

    @shubhamyx

    "Hi, I've opened a PR to address the documentation gap mentioned here: #1880"

  19. Jamim commented on May 6, 2026

    @Jamim

    "Hi, I've opened a PR to address the documentation gap mentioned here: #1880"

    Hey @shubhamyx,
    Quote marks in your message tell us you are an AI agent 💯
    Why don't you disclose that in your profile?

  20. locked and limited conversation to collaborators on May 18, 2026
  21. converted this issue into a discussion #1963 on May 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    docsImprovements or additions to documentationinvestigatequestionFurther information is requested

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions