Skip to content

SQLModel fails to create DB constraints when using Annotated types after Pydantic 2.12.0 #1623

Description

@YuriiMotov

Discussed in #1597

Originally posted by MarishLakshmanan October 8, 2025

First Check

  • I added a very descriptive title here.
  • I used the GitHub search to find a similar question 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

Environment:
Pydantic==2.12.0

Code:

from pydantic import StringConstraints
NonEmptyString = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]

Class Test(SQLModel):
    id: NonEmptyString = Field(primary_key=True) # this was working fine on pydantic v2.11.9

Description

Description

After upgrading to Pydantic 2.12.0, SQLModel no longer creates database constraints (like primary keys, unqiue keys ) for fields defined using Annotated types.

Example:

from typing import Annotated
from sqlmodel import SQLModel, Field
from pydantic import StringConstraints

NonEmptyString = Annotated[str, StringConstraints()]

class Test(SQLModel, table=True):
    id: NonEmptyString = Field(primary_key=True)

Expected Behavior

The table should be created with id as an varchar and primary key

Actual Behavior

it throws an error

sqlalchemy.exc.ArgumentError: Mapper Mapper[Test(test)] could not assemble any primary key columns for mapped table 'test'

Additional notes

In case you use an unique key constraint the table will be created, you will not get any error but the unique key constraint will not be created.

Operating System

Linux

Operating System Details

No response

SQLModel Version

0.0.26

Python Version

3.13.3

Additional Context

No response

Activity

  1. YuriiMotov commented on Oct 27, 2025

    @YuriiMotov
    MemberAuthor

    Test taken from #1603

    from sqlmodel import Field, SQLModel
    from typing_extensions import Annotated
    
    
    def test_declaration_syntax_1():
        class Person1(SQLModel):
            name: str = Field(primary_key=True)
    
        class Person1Final(Person1, table=True):
            pass
    
    
    def test_declaration_syntax_2():
        class Person2(SQLModel):
            name: Annotated[str, Field(primary_key=True)]
    
        class Person2Final(Person2, table=True):
            pass
    
    
    def test_declaration_syntax_3():
        class Person3(SQLModel):
            name: Annotated[str, ...] = Field(primary_key=True)
    
        class Person3Final(Person3, table=True):
            pass

    test_declaration_syntax_2 and test_declaration_syntax_3 work well with Pydantic 2.11.10, but fail with Pydantic 2.12

    PRs:

  2. self-assigned this
    on Oct 27, 2025
  3. removed their assignment
    on Oct 31, 2025
  4. Samreay commented on Nov 5, 2025

    @Samreay

    I think I just ran into this issue too, but in relation to timestamps with timezone:

    from datetime import datetime as dt
    from datetime import timezone as tz
    from typing_extensions import Annotated
    
    from sqlmodel import TIMESTAMP, Field, SQLModel
    from pydantic import BeforeValidator
    
    TIMESTAMP_WITH_TZ: type = TIMESTAMP(timezone=True)
    
    IntDatetime = Annotated[
        dt,
        BeforeValidator(lambda x: x if isinstance(x, dt) else dt.fromtimestamp(int(x) / 1000, tz=tz.utc)),
    ]
    
    # This model works with both 2.11.10 and 2.12.0
    class Message(SQLModel, table=True):
        time: dt = Field(sa_type=TIMESTAMP_WITH_TZ)
    
    # This model works with <=2.11.10, but 2.12.0 generates an error
    # `can't subtract offset-naive and offset-aware datetimes`
    # As the SQL generated is $1::TIMESTAMP WITHOUT TIME ZONE
    class Message(SQLModel, table=True):
        time: IntDatetime = Field(sa_type=TIMESTAMP_WITH_TZ)
  5. KrilleGH commented on Nov 12, 2025

    @KrilleGH

    I wanted to share this example which can also be used as a test.
    With Pydantic 2.12 it prints an empty set while it should print:
    {ForeignKey('vehicle.id')}

    from pydantic import PositiveInt
    
    from sqlmodel import Field, Session, SQLModel, col, select
    
    
    class Vehicle(SQLModel, table=True):
        name: str
        id: PositiveInt | None = Field(default=None, primary_key=True)
    
    
    class Drivers_Logbook(SQLModel, table=True):
        # To make this example succeed, replace the next line with either
        # vehicle_id: int = Field(foreign_key='vehicle.id')
        # vehicle_id: PositiveInt | None = Field(foreign_key='vehicle.id', default=None)
        vehicle_id: PositiveInt = Field(foreign_key='vehicle.id')
        id: PositiveInt | None = Field(default=None, primary_key=True)
    
    
    drivers_logbook = SQLModel.metadata.tables['drivers_logbook']
    print(drivers_logbook.foreign_keys)

    I also have a version using pure SQLAlchemy and couldn't reproduce the issue there.

  6. dwreeves commented on Nov 18, 2025

    @dwreeves

    Ran into this. The fundamental issue appears to be that the subclass sqlmodel.main.FieldInfo is no longer respected in Pydantic 2.12+. The object being passed to get_column_from_field is of type pydantic.fields.FieldInfo, whereas in previous versions it was the sqlmodel subclass. #1603 seems like a reasonable resolution as it restores the original sqlmodel.main.FieldInfo object to pass into everything else.

  7. frgfm commented on Nov 25, 2025

    @frgfm

    I just found out I had the same problem: no issue with pydantic==2.11.9, but trouble after that. For reference, in both cases I have the same version of sqlalchemy==2.0.44 and sqlmodel==0.0.27 in a locked container.

    My snippet for the regression:

    import uuid
    from sqlmodel import Field, SQLModel
    
    class User(SQLModel, table=True):
        __tablename__ = "users"
        id: Annotated[UUID4 | None, Field(default_factory=uuid.uuid4, primary_key=True)]
        name: str

    the error with pydantic==2.12.4:

    Traceback (most recent call last):
      File "/app/test.py", line 4, in <module>
        class User(SQLModel, table=True):
      File "/opt/venv/lib/python3.11/site-packages/sqlmodel/main.py", line 644, in __init__
        DeclarativeMeta.__init__(cls, classname, bases, dict_, **kw)
      File "/opt/venv/lib/python3.11/site-packages/sqlalchemy/orm/decl_api.py", line 199, in __init__
        _as_declarative(reg, cls, dict_)
      File "/opt/venv/lib/python3.11/site-packages/sqlalchemy/orm/decl_base.py", line 245, in _as_declarative
        return _MapperConfig.setup_mapping(registry, cls, dict_, None, {})
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "/opt/venv/lib/python3.11/site-packages/sqlalchemy/orm/decl_base.py", line 326, in setup_mapping
        return _ClassScanMapperConfig(
               ^^^^^^^^^^^^^^^^^^^^^^^
      File "/opt/venv/lib/python3.11/site-packages/sqlalchemy/orm/decl_base.py", line 581, in __init__
        self._early_mapping(mapper_kw)
      File "/opt/venv/lib/python3.11/site-packages/sqlalchemy/orm/decl_base.py", line 367, in _early_mapping
        self.map(mapper_kw)
      File "/opt/venv/lib/python3.11/site-packages/sqlalchemy/orm/decl_base.py", line 1995, in map
        mapper_cls(self.cls, self.local_table, **self.mapper_args),
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "<string>", line 2, in __init__
      File "/opt/venv/lib/python3.11/site-packages/sqlalchemy/util/deprecations.py", line 281, in warned
        return fn(*args, **kwargs)  # type: ignore[no-any-return]
               ^^^^^^^^^^^^^^^^^^^
      File "/opt/venv/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 866, in __init__
        self._configure_pks()
      File "/opt/venv/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 1652, in _configure_pks
        raise sa_exc.ArgumentError(
    sqlalchemy.exc.ArgumentError: Mapper Mapper[User(users)] could not assemble any primary key columns for mapped table 'users'
    
  8. raffaelemancuso commented on Dec 13, 2025

    @raffaelemancuso

    I have pinned pydantic to 2.11.9 in my pyproject.toml:

    dependencies = [
    	# pin pydantic to < 2.12
    	# see https://github.com/fastapi/sqlmodel/issues/1623
    	"sqlmodel==0.0.26",
    	"pydantic==2.11.9",
    

    and uv sync but I still get this error:

    class AnsDBRow(SQLModel, table=True):
        """Represents a question in the database."""
    
        __table_args__ = ({"extend_existing": True},)
        __tablename__ = "question"
    
        id: int|None = Field(primary_key=True)
    
        project_id: int = Field(index=True, nullable=False, unique=True)
        question: str
        retrieved_docs: str
        retrieved_docs_n: int
        raw_resp: str
        validation_error: bool
        is_ai: bool | None = Field(nullable=True)
        explanation: str | None = Field(nullable=True)
    
    ---------------------------------------------------------------------------
    ArgumentError                             Traceback (most recent call last)
    Cell In[17], [line 1](vscode-notebook-cell:?execution_count=17&line=1)
    ----> [1](vscode-notebook-cell:?execution_count=17&line=1) class AnsDBRow(SQLModel, table=True):
          2     """Represents a question in the database."""
          4     __table_args__ = ({"extend_existing": True},)
    
    File c:\data\papers\paper_5_AIPUB_RM\.venv\Lib\site-packages\sqlmodel\main.py:644, in SQLModelMetaclass.__init__(cls, classname, bases, dict_, **kw)
        640         setattr(cls, rel_name, rel_value)  # Fix #315
        641     # SQLAlchemy no longer uses dict_
        642     # Ref: https://github.com/sqlalchemy/sqlalchemy/commit/428ea01f00a9cc7f85e435018565eb6da7af1b77
        643     # Tag: 1.4.36
    --> [644](file:///C:/data/papers/paper_5_AIPUB_RM/.venv/Lib/site-packages/sqlmodel/main.py:644)     DeclarativeMeta.__init__(cls, classname, bases, dict_, **kw)
        645 else:
        646     ModelMetaclass.__init__(cls, classname, bases, dict_, **kw)
    
    File c:\data\papers\paper_5_AIPUB_RM\.venv\Lib\site-packages\sqlalchemy\orm\decl_api.py:199, in DeclarativeMeta.__init__(cls, classname, bases, dict_, **kw)
        196         cls._sa_registry = reg
        198 if not cls.__dict__.get("__abstract__", False):
    --> [199](file:///C:/data/papers/paper_5_AIPUB_RM/.venv/Lib/site-packages/sqlalchemy/orm/decl_api.py:199)     _as_declarative(reg, cls, dict_)
        200 type.__init__(cls, classname, bases, dict_)
    
    File c:\data\papers\paper_5_AIPUB_RM\.venv\Lib\site-packages\sqlalchemy\orm\decl_base.py:245, in _as_declarative(registry, cls, dict_)
        240 def _as_declarative(
        241     registry: _RegistryType, cls: Type[Any], dict_: _ClassDict
        242 ) -> Optional[_MapperConfig]:
        243     # declarative scans the class for attributes.  no table or mapper
        244     # args passed separately.
    --> [245](file:///C:/data/papers/paper_5_AIPUB_RM/.venv/Lib/site-packages/sqlalchemy/orm/decl_base.py:245)     return _MapperConfig.setup_mapping(registry, cls, dict_, None, {})
    
    File c:\data\papers\paper_5_AIPUB_RM\.venv\Lib\site-packages\sqlalchemy\orm\decl_base.py:326, in _MapperConfig.setup_mapping(cls, registry, cls_, dict_, table, mapper_kw)
        322     return _DeferredMapperConfig(
        323         registry, cls_, dict_, table, mapper_kw
        324     )
        325 else:
    --> [326](file:///C:/data/papers/paper_5_AIPUB_RM/.venv/Lib/site-packages/sqlalchemy/orm/decl_base.py:326)     return _ClassScanMapperConfig(
        327         registry, cls_, dict_, table, mapper_kw
        328     )
    
    File c:\data\papers\paper_5_AIPUB_RM\.venv\Lib\site-packages\sqlalchemy\orm\decl_base.py:581, in _ClassScanMapperConfig.__init__(self, registry, cls_, dict_, table, mapper_kw)
        577 self._setup_table(table)
        579 self._setup_inheriting_columns(mapper_kw)
    --> [581](file:///C:/data/papers/paper_5_AIPUB_RM/.venv/Lib/site-packages/sqlalchemy/orm/decl_base.py:581) self._early_mapping(mapper_kw)
    
    File c:\data\papers\paper_5_AIPUB_RM\.venv\Lib\site-packages\sqlalchemy\orm\decl_base.py:367, in _MapperConfig._early_mapping(self, mapper_kw)
        366 def _early_mapping(self, mapper_kw: _MapperKwArgs) -> None:
    --> [367](file:///C:/data/papers/paper_5_AIPUB_RM/.venv/Lib/site-packages/sqlalchemy/orm/decl_base.py:367)     self.map(mapper_kw)
    
    File c:\data\papers\paper_5_AIPUB_RM\.venv\Lib\site-packages\sqlalchemy\orm\decl_base.py:1995, in _ClassScanMapperConfig.map(self, mapper_kw)
       1990 else:
       1991     mapper_cls = Mapper
       1993 return self.set_cls_attribute(
       1994     "__mapper__",
    -> [1995](file:///C:/data/papers/paper_5_AIPUB_RM/.venv/Lib/site-packages/sqlalchemy/orm/decl_base.py:1995)     mapper_cls(self.cls, self.local_table, **self.mapper_args),
       1996 )
    
    File <string>:2, in __init__(self, class_, local_table, properties, primary_key, non_primary, inherits, inherit_condition, inherit_foreign_keys, always_refresh, version_id_col, version_id_generator, polymorphic_on, _polymorphic_map, polymorphic_identity, concrete, with_polymorphic, polymorphic_abstract, polymorphic_load, allow_partial_pks, batch, column_prefix, include_properties, exclude_properties, passive_updates, passive_deletes, confirm_deleted_rows, eager_defaults, legacy_is_orphan, _compiled_cache_size)
    
    File c:\data\papers\paper_5_AIPUB_RM\.venv\Lib\site-packages\sqlalchemy\util\deprecations.py:281, in deprecated_params.<locals>.decorate.<locals>.warned(fn, *args, **kwargs)
        274     if m in kwargs:
        275         _warn_with_version(
        276             messages[m],
        277             versions[m],
        278             version_warnings[m],
        279             stacklevel=3,
        280         )
    --> [281](file:///C:/data/papers/paper_5_AIPUB_RM/.venv/Lib/site-packages/sqlalchemy/util/deprecations.py:281) return fn(*args, **kwargs)
    
    File c:\data\papers\paper_5_AIPUB_RM\.venv\Lib\site-packages\sqlalchemy\orm\mapper.py:866, in Mapper.__init__(self, class_, local_table, properties, primary_key, non_primary, inherits, inherit_condition, inherit_foreign_keys, always_refresh, version_id_col, version_id_generator, polymorphic_on, _polymorphic_map, polymorphic_identity, concrete, with_polymorphic, polymorphic_abstract, polymorphic_load, allow_partial_pks, batch, column_prefix, include_properties, exclude_properties, passive_updates, passive_deletes, confirm_deleted_rows, eager_defaults, legacy_is_orphan, _compiled_cache_size)
        864 self._configure_properties()
        865 self._configure_polymorphic_setter()
    --> [866](file:///C:/data/papers/paper_5_AIPUB_RM/.venv/Lib/site-packages/sqlalchemy/orm/mapper.py:866) self._configure_pks()
        867 self.registry._flag_new_mapper(self)
        868 self._log("constructed")
    
    File c:\data\papers\paper_5_AIPUB_RM\.venv\Lib\site-packages\sqlalchemy\orm\mapper.py:1652, in Mapper._configure_pks(self)
       1647 # otherwise, see that we got a full PK for the mapped table
       1648 elif (
       1649     self.persist_selectable not in self._pks_by_table
       1650     or len(self._pks_by_table[self.persist_selectable]) == 0
       1651 ):
    -> [1652](file:///C:/data/papers/paper_5_AIPUB_RM/.venv/Lib/site-packages/sqlalchemy/orm/mapper.py:1652)     raise sa_exc.ArgumentError(
       1653         "Mapper %s could not assemble any primary "
       1654         "key columns for mapped table '%s'"
       1655         % (self, self.persist_selectable.description)
       1656     )
       1657 elif self.local_table not in self._pks_by_table and isinstance(
       1658     self.local_table, schema.Table
       1659 ):
       1660     util.warn(
       1661         "Could not assemble any primary "
       1662         "keys for locally mapped table '%s' - "
       1663         "no rows will be persisted in this Table."
       1664         % self.local_table.description
       1665     )
    
    ArgumentError: Mapper Mapper[AnsDBRow(question)] could not assemble any primary key columns for mapped table 'question'
    

    Any idea why?

  9. KrilleGH commented on Dec 14, 2025

    @KrilleGH

    Any idea why?

    You didn't use Annotated types (unless you rebound the built in types), so this is a different issue.

    It may be related to your usage of extend_existing or you're missing a default for the id column, but it doesn't belong here. You could open a new discussion.

    However I hope the issue here will be resolved soon as it blocks upgrading to Python 3.14.

  10. varadinov commented on Jan 10, 2026

    @varadinov

    I receive the same error after upgrading python to 3.14 and pydantic to the new version.
    pydantic 2.12.5
    pydantic-core 2.41.5
    pydantic-extra-types 2.11.0
    pydantic-settings 2.12.0

    This is the definition:
    class MyModel(SQLModel, table=True):
    id: UUID4 = Field(default_factory=uuid4, primary_key=True, index=True, nullable=False)
    description: str = Field(default="Default Description", nullable=False)
    ...

  11. iangoldby commented on Jan 16, 2026

    @iangoldby

    Is this the same issue that prevents the 'new style' Pydantic Annotated pattern from working?

    For example, if I define a field in a table model as:

        foo: Annotated[int, Field(sa_type=BigInteger)]
    

    then it ignores the sa_type and the column is created with a regular INT type. But if I use

        foo: int = Field(sa_type=BigInteger)
    

    then it works as expected.

  12. KrilleGH commented on Jan 22, 2026

    @KrilleGH
        foo: Annotated[int, Field(sa_type=BigInteger)]

    I think the issue is that you mixed field and type metadata. As mentioned in the Tip box in the section of the Pydantic docs you linked, you annotated the int type with sa_type and it doesn't have any effect there.

    You need to put that on the field foo, as done in your second example. I'm not sure it could be written differently with the same effect.

  13. iangoldby commented on Jan 22, 2026

    @iangoldby

    @KrilleGH In both cases the sa_type is applied to the FieldInfo definition. But in the first case it is injected into the model via a 'default' value of the model field (which is a little bit of a hack/abuse) and the other via metadata on the model field type. Pydantic understands both approaches, but it appears that SQLModel doesn't (yet?) understand the latter. Since SQLModel has other issues with type metadata introduced via Annotated (the discussion above) I wondered if these two issues were actually the same thing.

  14. FelixYe0509 commented on Jan 28, 2026

    @FelixYe0509

    Just use pydantic<=2.11.9

  15. KrilleGH commented on Jan 29, 2026

    @KrilleGH

    Just use pydantic<=2.11.9

    I thoght the previous text with the detailed description that was here before is much more interesting. Could you bring it back?

  16. FelixYe0509 commented on Jan 29, 2026

    @FelixYe0509

    只需使用 pydantic<=2.11.9 即可。

    我觉得之前那段描述详尽的文字更有意思。您能把它恢复过来吗?

    At the beginning, i thought its' because pydantic/pydantic@d3a57eb

    this fix change the FieldInfo copy logic, don't use copy.copy() to clone the FieldInfo

    if type(self) is FieldInfo: # Fast-path if the instance isn't a subclass (copy.copy()relies on pickling which is slower): copied = FieldInfo.__new__(FieldInfo) for attr_name in FieldInfo.__slots__: setattr(copied, attr_name, getattr(self, attr_name

    but when i check the commit date i realize it still not release

    so i delete what i wrote

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionFurther information is requested

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions