Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
7 of 8 tasks
sorasful opened this issue Oct 16, 2021 · 10 comments
Open
7 of 8 tasks
Labels
question Further information is requested

Comments

@sorasful
Copy link

sorasful commented Oct 16, 2021

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

@sorasful sorasful added the question Further information is requested label Oct 16, 2021
@sorasful
Copy link
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
        ):

@mudassirkhan19
Copy link

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.

@chriswhite199
Copy link
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

@rickerp
Copy link

rickerp commented Dec 10, 2021

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

@AndrewRiggs-Atkins
Copy link

AndrewRiggs-Atkins commented Jan 12, 2022

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

@synodriver
Copy link

synodriver commented Jun 15, 2022

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

@eudu
Copy link

eudu commented Jun 30, 2022

Duplicate of #52

@eseglem
Copy link

eseglem commented Aug 17, 2022

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.

@Jaakkonen
Copy link

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)

@Tanner-Ray-Martin
Copy link

Tanner-Ray-Martin commented Aug 14, 2024


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())

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

10 participants