TypeID¶
activemodel includes first-class support for TypeIDs — type-safe, globally unique identifiers with a human-readable prefix (e.g. user_01h45ytscbebyvny4gc8cr8ma2).
Primary Key¶
Use TypeIDPrimaryKey with a TypeIDField annotation to define a typed primary key:
from typing import Literal
from typeid import TypeID
from activemodel import BaseModel
from activemodel.mixins import TypeIDField, TypeIDPrimaryKey
class User(BaseModel, table=True):
id: TypeIDField[Literal["user"]] = TypeIDPrimaryKey("user")
The Literal["user"] annotation gives static type-checking of the prefix. Each prefix must be unique within a process — defining two models with the same prefix raises an AssertionError at class definition time.
Foreign Keys¶
Use Model.foreign_key() to wire up a FK column. Annotate with TypeID (not TypeIDType):
from typing import Literal
from typeid import TypeID
from activemodel import BaseModel
from activemodel.mixins import TypeIDField, TypeIDPrimaryKey
from sqlmodel import Relationship
class Distribution(BaseModel, table=True):
id: TypeIDField[Literal["dst"]] = TypeIDPrimaryKey("dst")
name: str
class Screening(BaseModel, table=True):
id: TypeIDField[Literal["scr"]] = TypeIDPrimaryKey("scr")
distribution_id: TypeID = Distribution.foreign_key(index=True)
distribution: Distribution = Relationship()
foreign_key() defaults to nullable=False and accepts any Field kwargs (index, nullable, etc.).
Prefix Enforcement¶
For FK fields that must carry a specific prefix — such as self-referential foreign keys — use sa_type=TypeIDType(prefix="..."). The annotation stays as TypeID:
from typeid import TypeID
from activemodel import BaseModel
from activemodel.mixins import TypeIDField, TypeIDPrimaryKey
from activemodel.types import TypeIDType
from sqlmodel import Field
class Screening(BaseModel, table=True):
id: TypeIDField[Literal["scr"]] = TypeIDPrimaryKey("scr")
merged_into_screening_id: TypeID | None = Field(
default=None,
sa_type=TypeIDType(prefix="scr"), # type: ignore
)
TypeIDType appears only in sa_type=, never as the field annotation. Assigning a TypeID with the wrong prefix raises TypeIDValidationError.
Raw Polymorphic References¶
Use TypeIDType.raw() when a field can reference objects of multiple different types (and thus different TypeID prefixes). The field accepts TypeIDs of any prefix on write, but only the underlying UUID is persisted — on read it returns a plain UUID with no prefix attached.
from uuid import UUID
from typing import Literal
from typeid import TypeID
from activemodel import BaseModel
from activemodel.mixins import TypeIDPrimaryKey
from activemodel.types import TypeIDType
from sqlmodel import Field
class Event(BaseModel, table=True):
id: TypeIDField[Literal["event"]] = TypeIDPrimaryKey("event")
originating_id: UUID | None = Field(
default=None,
index=True,
sa_type=TypeIDType.raw(),
)
The field accepts TypeID objects, TypeID strings ("prefix_xxx"), stdlib UUID, and bare UUID strings on write. Because the field reads back as a stdlib UUID while TypeID objects are not directly comparable to UUID, use .bytes for comparisons:
assert event.originating_id.bytes == some_typeid.uuid_bytes
Raw fields emit {"type": "string", "format": "uuid"} in the OpenAPI schema (not typeid).
Looking Up Records¶
Model.get() accepts any of these equivalent forms:
User.get(user_id) # TypeID instance
User.get(str(user_id)) # TypeID string, e.g. "user_01h45y..."
User.get(user_id.uuid) # uuid_utils.UUID
User.get(uuid_obj) # stdlib UUID
User.get("01h45y...") # bare UUID string
Pydantic / OpenAPI¶
Prefixed TypeID fields emit {"type": "string", "format": "typeid"} in the OpenAPI schema. Pydantic accepts both TypeID instances and plain strings (coerced via TypeID.from_string). In Python mode, model_dump() preserves TypeID objects; in JSON mode (model_dump(mode="json") or model_dump_json()), they serialize to their string representation.