Skip to content

Pydantic Validators does not raise ValueError if conditions are not met #134

Closed as duplicate

Description

@sorasful

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

class Playlist(SQLModel, table=True):
    __tablename__ = 'playlists'

    id: Optional[int] = Field(default=None, primary_key=True)
    title: str
    description: Optional[str]
    

    @validator('title')
    def title_not_too_long(cls, v):
        if len(v) > 255:
            raise ValueError('too long')
        return v



p = Playlist(title="x" * 300, description="OK")
p.title # None
p.description # 'OK'

Description

When the condition is not met, the code just returns None for the field instead of raising an error.

The other fields are well populated.

Operating System

Windows

Operating System Details

No response

SQLModel Version

0.0.4

Python Version

3.9.2

Additional Context

No response

Activity

  1. sorasful commented on Oct 16, 2021

    @sorasful
    Author

    It seems it related to this part of the code :

    main.py : line 491

            # Only raise errors if not a SQLModel model
            if (
                not getattr(__pydantic_self__.__config__, "table", False)
                and validation_error
            ):
  2. mudassirkhan19 commented on Oct 28, 2021

    @mudassirkhan19

    I'm facing a similar issue with max_length param in Field. I filed an issue here: #148, then realised this is a similar issue so closed it.

  3. chriswhite199 commented on Nov 24, 2021

    @chriswhite199
    Contributor

    I've seen this in another ticket, and while not explicitly documented, https://sqlmodel.tiangolo.com/tutorial/fastapi/multiple-models/ notes to use multiple models and inheritance.

    For your specific example, refactoring as the following would provide validation:

    from typing import Optional
    
    from pydantic import ValidationError
    from sqlmodel import SQLModel, Field, create_engine, Session
    
    
    class Playlist(SQLModel):
        id: Optional[int] = Field(default=None, primary_key=True)
        title: str = Field(max_length=255)
        description: Optional[str]
    
    class DbPlaylist(Playlist, table=True):
        __tablename__ = 'playlists'
    
    # this will now raise a ValidationError:
    try:
        p = Playlist(title="x" * 300, description="OK")
        assert False
    except ValidationError:
        pass
    
    p = Playlist(title="x" * 255, description="OK")
    
    engine = create_engine(url='sqlite://', echo=True)
    SQLModel.metadata.create_all(engine)
    with Session(engine) as session:
        # You will need to convert the base class to the mapped instance though
        mapped_p = DbPlaylist.from_orm(p)
        session.add(mapped_p)
        session.commit()
        # and remember that the mapped playlist will have the auto-incremented ID set, not the original
        # playlist instance
        assert p.id is None
        assert mapped_p.id == 1
  4. rickerp commented on Dec 10, 2021

    @rickerp

    Any news on this? For me this is kind of a security issue, as I am expecting to not create a new entry and it tries to create

  5. AndrewRiggs-Atkins commented on Jan 12, 2022

    @AndrewRiggs-Atkins

    I had had a similar issue where I was wanting to use regular pydantic validation on initialisation instead of only when being commited into the database. While I'm not sure of the other impacts it may cause, my solution is below.

    main.py > SQLModelMetaclass > new > line 307

    config_validate = get_config("validate")
    if config_validate is True:
        # If it was passed by kwargs, ensure it's also set in config
        new_cls.__config__.validate = config_validate
        for k, v in new_cls.__fields__.items():
            col = get_column_from_field(v)
            setattr(new_cls, k, col)

    main.py > SQLModel > init > line 517

    if (
        (not getattr(__pydantic_self__.__config__, "table", False)
        or getattr(__pydantic_self__.__config__, "validate", False)) # Added validate
        and validation_error
    ):
        raise validation_error

    usage

    class Hero(SQLModel, table=True, validate=True): # Added validate
    
        id: int = Field(primary_key=True, nullable=False)
        number: int
    
        @validator('number')
        def less_than_100(cls, v):
            if v > 100:
                raise ValueError('must be less than 100')
            return v

    When validate is disabled, validation runs on commit as usual, with it enabled, validation runs on object initialisation. Works for pydanic @validate functions as well as others such as max_length

  6. synodriver commented on Jun 15, 2022

    @synodriver

    Mine situation is more strange, the validators are not runing at all! Maybe it's related to this, but after I tried @AndrewRiggs-Atkins's patch they still not working.

    Here is my code and I'm using async mode.

    class Pic(SQLModel, table=True, validate=True):
        __tablename__ = "main"
    
        pid: Optional[int]
        p: Optional[int]
        uid: Optional[int]
        title: Optional[str]
        author: Optional[str]
        r18: bool
        width: Optional[int]
        height: Optional[int]
        tags: Optional[str]
        ext: Optional[str]
        uploadDate: Optional[int]
        urls: Optional[str] = Field(primary_key=True)
    
        @validator("r18", pre=True, always=True)
        def check_r18(cls, v) -> bool:
            print("hi")
            if isinstance(v, str):
                return True if v == "True" else False
            return v
    
  7. eudu commented on Jun 30, 2022

    @eudu

    Duplicate of #52

  8. eseglem commented on Aug 17, 2022

    @eseglem

    I was testing some other types out when I got really confused because you can initialize an a model with required fields without providing any of them. So I dug in and I realized the validation wasn't happening at all. Definitely an unexpected behavior from my perspective. I am sure there was a reason behind the decision, but I don't know what that reason was. I want everything to validate all the time, and if I didn't want to I would use the construct method to avoid it.

    Could work around it with an additional class, but a big part of why I was looking at SQLModel was the ability to reduce the number of classes. Its still better than the alternative probably, but the unexpected behavior may cause some issues in the future.

  9. Jaakkonen commented on Jun 12, 2023

    @Jaakkonen

    The maintainer proposed a good approach here #52 (comment)

    The reason for not running validators is SQLAlchemy assigning stuff into relationships/etc after instantiating the class. If it's possible I'd find doing the initialization using Model.construct() first and then passing the final object to the validators after SQLAlchemy magic better.

    Now the act of creating another model for data validation and then passing the values to the actual table-model can be done something like this:

    class MySQLModel(SQLModel, table=False):
      @classmethod
      def create(cls, **kwargs):
        class Validator(cls, table=False):
          class Config:
            table = False
        # Check if all fields are set
        Validator(**kwargs)
        return cls(**kwargs)
  10. Tanner-Ray-Martin commented on Aug 14, 2024

    @Tanner-Ray-Martin

    I ran into a similar issue recently when trying to display models as various FastUI forms. Here's my workaround:

    from typing import Optional
    from sqlmodel import SQLModel, Field
    from datetime import datetime as dt
    from pydantic import BaseModel, model_validator
    
    # Form Definition
    class DbInfoForm(SQLModel):
        name: str = Field(
            title="Database Name",
            description="The common name for your database",
        )
        short_name: str = Field(
            title="Database Short Name",
            description="The short name for the database.",
        )
        display_name: str = Field(
            title="Database Display Name",
            description="The display name of the database.",
        )
        category: Optional[str] = Field(
            title="Database Category",
            description="Category of Database. For example, your department or the type of records.",
        )
        alias: Optional[str] = Field(
            title="Database Alias",
            description="What other people or systems might refer to your database or category as.",
        )
        description: Optional[str] = Field(
            title="Detailed Description",
            description="Write a detailed description of this database and its purpose.",
        )
    
    # Model Definition That performs Validation
    class DbInfo(DbInfoForm):
        id: Optional[int] = Field(default=None, primary_key=True)
        created_at: dt = Field(default=dt.now())
        updated_at: dt = Field(default_factory=dt.now)
        status: Optional[str] = Field(default="building", max_length=100)
    
        @model_validator(mode="before")
        @classmethod
        def remove_ms_from_dt(cls, data: Any) -> Any:
            if isinstance(data, dict):
                for field_name, field_value in data.items():
                    if isinstance(field_value, dt):
                        data.update({field_name: field_value.replace(microsecond=0)})
                else:
                    assert isinstance(data, dict)
            return data
    
    # Model with Table Configuration
    class DbInfoModel(DbInfo, table=True):
        ...

    Usage Example

    The DbInfo class should only be required before DbInfoModel when creating or updating the database.

    Creating Database:

    db_info_form = DbInfoForm(
        name="PyTestDB",
        short_name="PTDB",
        display_name="PyTest Database",
        category="Testing",
        alias="Some Alias for PytestDB",
        description="This is a pytest database",
    )
    
    validated_form = DbInfo(**db_info_form.model_dump())
    
    form_to_add = DbInfoModel(**validated_form.model_dump())

  11. YuriiMotov commented on Aug 27, 2025

    @YuriiMotov
    Member

    This is a duplicate of #52
    And there is more recent #453 on the same topic

    Let's close this issue in favor of #453 as has higher support (more reactions)

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

    questionFurther information is requested

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions