Skip to content

Conversation

proever
Copy link

@proever proever commented Mar 26, 2024

Currently, it is not possible to specify a SQLModel with an Optional complex type, such as a Decimal, as follows:

from decimal import Decimal
from typing import Annotated
from sqlmodel import Field, SQLModel


class PriceModel(SQLModel, table=True):
    id: int | None = Field(primary_key=True, default=None)
    price: Annotated[Decimal, Field(decimal_places=2, max_digits=9)] | None = None

Doing so results in the following error (see #67, #312) in get_sqlalchemy_type:

TypeError: issubclass() arg 1 must be a class

This PR attempts to fix this issue by adding a check in get_sqlalchemy_type for whether the type of the Field is a typing_extensions.AnnotatedAlias. If it is, instead of using the class of the AnnotatedAlias for the following comparisons (which results in the above error), it uses AnnotatedAlias.__origin__. Similarly, it infers the metadata from AnnotatedAlias.__metadata__ instead of the AnnotatedAlias itself.

In my testing, this approach seems to work well. I also added a simple test here that checks whether restrictions on an optional annotated field are enforced on the database side. It could probably be improved or extended.

@proever
Copy link
Author

proever commented Mar 26, 2024

looks like some (many) tests are failing, I'll try to fix them!

Is this feature something that should work with Pydantic v1 too? I'm not too familiar with it.

@bootc
Copy link

bootc commented Jun 4, 2024

I've just hit exactly this problem; is there any chance you could update your MR please @proever?

@alejsdev alejsdev added the feature New feature or request label Jul 12, 2024
@msftcangoblowm
Copy link

msftcangoblowm commented Jul 7, 2025

Would argue this PR fixes a bug and is not a FR

pydantic validators is a core reason for the existence of both pydantic and SQLModel.

SQLModel currently only supports validators using decorator-pattern

Which is neither DRY nor reusable

SQLModel does not support reusable Validators using annotated-pattern

This PR would fix this.

Example pydantic validator

def validator_is_dead_or_cursed(value: int) -> int:
    if value > 120:
        msg_warn = (
            f"{value!s} indicates this person very likely dead "
            "or bibically cursed"
        )
        raise ValueError(msg_warn)

    return value

NotDead = Annotated[int, pydantic.AfterValidator(validator_is_dead_or_cursed)]

Apply to Field

age: Optional[NotDead] = Field(default=None, gt=0)

or

age: Annotated[Optional[int], pydantic.AfterValidator(validator_is_dead_or_cursed)] = Field(default=None, gt=0)

Equivalent to age: Optional[int] = Field(default=None, gt=0, le=120)

Copy link
Contributor

@YuriiMotov YuriiMotov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I see it, this PR is basically about fixing the following scenario:

MyType: TypeAlias = Annotated[str, Field(max_length=5)]

class PriceModel(SQLModel, table=True):
    id: int | None = Field(primary_key=True, default=None)
    working: MyType  # VARCHAR(5)
    not_working: Optional[MyType] = None  # VARCHAR(255)

You are going to open a Pandora's box :)
I think this PR only handles one specific case, but in general we need to merge all nested Field annotations and handle conflicts. This seems to be a lot of work and quite error-prone.
Although, it would be nice to support this one day.

I suggest we skip it for now and get back to this feature later.

Seems to be related: #1281


As for initial code example, you can make it working by moving | None inside Annotated:

    price: Annotated[Decimal | None, Field(decimal_places=2, max_digits=9)] = None

@proever, thanks for your interest and efforts!

@@ -653,13 +659,24 @@ def get_sqlalchemy_type(field: Any) -> Any:
return sa_type

type_ = get_sa_type_from_field(field)
metadata = get_field_metadata(field)
if isinstance(type_, _AnnotatedAlias):
class_to_compare = type_.__origin__
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to use_type?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants