Skip to content

support optional annotated field with default value #862

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

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)

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.

5 participants