Pydantic Validators does not raise ValueError if conditions are not met
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
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
):
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.
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
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
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
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
Duplicate of https://github.com/tiangolo/sqlmodel/issues/52
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.
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)