sqlmodel icon indicating copy to clipboard operation
sqlmodel copied to clipboard

Pydantic Validators does not raise ValueError if conditions are not met

Open sorasful opened this issue 4 years ago • 10 comments

First Check

  • [X] I added a very descriptive title to this issue.
  • [X] I used the GitHub search to find a similar issue and didn't find it.
  • [X] I searched the SQLModel documentation, with the integrated search.
  • [X] 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.
  • [X] I already checked if it is not related to SQLModel but to Pydantic.
  • [X] I already checked if it is not related to SQLModel but to SQLAlchemy.

Commit to Help

  • [X] 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 avatar Oct 16 '21 19:10 sorasful

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

sorasful avatar Oct 16 '21 19:10 sorasful

I'm facing a similar issue with max_length param in Field. I filed an issue here: https://github.com/tiangolo/sqlmodel/issues/148, then realised this is a similar issue so closed it.

mudassirkhan19 avatar Oct 28 '21 16:10 mudassirkhan19

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

chriswhite199 avatar Nov 24 '21 23:11 chriswhite199

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

rickerp avatar Dec 10 '21 11:12 rickerp

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

AndrewRiggs-Atkins avatar Jan 12 '22 17:01 AndrewRiggs-Atkins

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

synodriver avatar Jun 15 '22 15:06 synodriver

Duplicate of https://github.com/tiangolo/sqlmodel/issues/52

eudu avatar Jun 30 '22 13:06 eudu

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.

eseglem avatar Aug 17 '22 15:08 eseglem

The maintainer proposed a good approach here https://github.com/tiangolo/sqlmodel/issues/52#issuecomment-1311987732

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)

Jaakkonen avatar Jun 12 '23 17:06 Jaakkonen