SQLModel fails to create DB constraints when using Annotated types after Pydantic 2.12.0
Discussed in https://github.com/fastapi/sqlmodel/discussions/1597
Originally posted by MarishLakshmanan October 8, 2025
First Check
- [X] I added a very descriptive title here.
- [X] I used the GitHub search to find a similar question 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.
- [X] 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
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
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:
- #1603
- https://github.com/fastapi/sqlmodel/pull/1607
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)
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.
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.
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'
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?
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.