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.