Skip to content

Defining Models

Overview

Every DynamoDB table maps to a DynamoModel subclass — a Pydantic BaseModel with table metadata attached via the @table() decorator. You get Pydantic validation, type hints, and serialization for free.

@table() decorator

from aiodynamodb import DynamoModel, table


@table("users", hash_key="user_id")
class User(DynamoModel):
    user_id: str
    name: str
    email: str | None = None

Parameters

Parameter Type Description
name str DynamoDB table name
hash_key str Field name used as the partition key
range_key str | None Field name used as the sort key (optional)
indexes list[GSI | LSI] | None Secondary indexes (optional)

Composite key tables

@table("orders", hash_key="order_id", range_key="created_at")
class Order(DynamoModel):
    order_id: str
    created_at: str
    total: int

With a range_key, operations that accept a key require both hash_key and range_key.

Supported key types

The hash_key and range_key fields can be typed as:

Python type DynamoDB type
str String (S)
int Number (N)
float Number (N)
bytes Binary (B)
datetime String (S) — ISO format
IntEnum subclasses Number (N)
StrEnum subclasses String (S)
Literal[...] Type inferred from its values; all must map to the same supported DynamoDB key type
Timestamp Number (N) — Unix seconds
TimestampMillis Number (N) — Unix milliseconds
TimestampMicros Number (N) — Unix microseconds
TimestampNanos Number (N) — Unix nanoseconds

See Custom Types for timestamp and JSON field details.

Literal keys work with both decorator arguments and key annotations, for example pk: HashKey[Literal["user"]] and sk: RangeKey[Literal[1, 2]] (import Literal from typing). They also work as secondary index keys. Mixed DynamoDB types such as Literal["user", 1], and unsupported values such as booleans or None, are rejected during table creation. Pydantic retains the literal constraints when validating model instances.

Enum key fields should inherit from enum.IntEnum or enum.StrEnum:

from enum import IntEnum

from aiodynamodb import DynamoModel, HashKey, RangeKey, table


class UserType(IntEnum):
    customer = 1
    admin = 2


@table("user_versions")
class UserVersion(DynamoModel):
    user_id: HashKey[str]
    user_type: RangeKey[UserType]
    name: str


await db.put(UserVersion(user_id="u1", user_type=UserType.admin, name="Alice"))
user = await db.get(UserVersion, hash_key="u1", range_key=UserType.admin)

Field types

Non-key fields can use values that resolve to DynamoDB-compatible types during Pydantic's Python-mode serialization:

Python type DynamoDB type
None Null (NULL)
bool Boolean (BOOL)
str String (S)
int, float, Decimal Number (N)
bytes Binary (B)
datetime ISO-8601 String (S)
Enum The recursively serialized enum value
list, tuple List (L)
Non-empty homogeneous set, frozenset String, Number, or Binary Set (SS/NS/BS)
dict[str, T], nested Pydantic models Map (M)

DynamoDB map keys must be strings. Sets cannot be empty and must contain only strings, numbers, or bytes of a single type.

Other Pydantic-compatible types need a Python-mode PlainSerializer that returns one of the supported values above. A serializer configured only with when_used="json" is not applied because DynamoDB models are dumped in Python mode.

Some examples:

from datetime import datetime
from aiodynamodb import DynamoModel, table
from aiodynamodb.custom_types import Timestamp, JSONStr
from pydantic import BaseModel


class Address(BaseModel):
    street: str
    city: str


@table("profiles", hash_key="profile_id")
class Profile(DynamoModel):
    profile_id: str
    created_at: Timestamp  # stored as Unix seconds integer
    address: Address  # stored as a DynamoDB Map
    tags: list[str]  # stored as a DynamoDB List
    metadata: JSONStr[Address]  # stored as a JSON string in DynamoDB
    score: float  # stored as Number; float → Decimal handled automatically

What @table does

The decorator attaches a Meta class variable (TableMeta) to your model:

User.Meta.table_name  # "users"
User.Meta.hash_key  # "user_id"
User.Meta.range_key  # None
User.Meta.global_secondary_indexes  # {}
User.Meta.local_secondary_indexes  # {}

Serialization

DynamoModel has two serialization paths:

  • to_dynamo() — serializes to DynamoDB AttributeValue objects (wire format, used by transact/batch operations)
  • to_dynamo_compatible() — recursively normalizes values to DynamoDB-compatible Python types (used by table-level resource operations)
  • from_dynamo(raw) — deserializes from AttributeValue objects back to a model instance

These are called internally by the client — you rarely need to invoke them directly.