Custom Types¶
aiodynamodb ships several Pydantic type annotations in aiodynamodb.custom_types that control how values are serialized to and from DynamoDB.
Timestamp types¶
All timestamp types annotate a datetime field. They differ only in the integer precision stored in DynamoDB as a Number (N).
| Type | DynamoDB storage | Formula |
|---|---|---|
Timestamp |
Unix seconds (int) | int(d.timestamp()) |
TimestampMillis |
Unix milliseconds (int) | int(d.timestamp() * 1_000) |
TimestampMicros |
Unix microseconds (int) | int(d.timestamp() * 1_000_000) |
TimestampNanos |
Unix nanoseconds (int) | int(d.timestamp() * 1_000_000_000) |
Usage¶
from datetime import datetime
from aiodynamodb import DynamoModel, table
from aiodynamodb.custom_types import Timestamp, TimestampMillis
@table("events", hash_key="event_id", range_key="occurred_at")
class Event(DynamoModel):
event_id: str
occurred_at: Timestamp # stored as integer seconds in DynamoDB
processed_at: TimestampMillis | None = None
event = Event(
event_id="e1",
occurred_at=datetime(2026, 1, 1, 12, 0, 0), # stored as 1767254400
processed_at=None,
)
await db.put(event)
As key types¶
Timestamp types work as hash_key or range_key fields:
@table("events", hash_key="event_id", range_key="occurred_at")
class Event(DynamoModel):
event_id: str
occurred_at: Timestamp
When you call db.get() or db.update() with a datetime as the key value, the client serializes it to the correct integer automatically.
JSONStr¶
JSONStr[T] stores a Pydantic model as a JSON string in DynamoDB (a String/S attribute), and deserializes it back transparently on read.
from pydantic import BaseModel
from aiodynamodb.custom_types import JSONStr
class Address(BaseModel):
street: str
city: str
@table("profiles", hash_key="profile_id")
class Profile(DynamoModel):
profile_id: str
address: JSONStr[Address] # stored as '{"street":"...","city":"..."}'
Use this when you want to store nested structured data as a string (for example, to fit within a field that has a string constraint elsewhere, or to avoid DynamoDB Map overhead for deeply nested structures).
KeyT¶
KeyT is the union of all types accepted as hash_key and range_key values in client method calls:
type KeyT = int | str | Timestamp | TimestampMillis | TimestampMicros | TimestampNanos | datetime | IntEnum | StrEnum
You don't use KeyT in model field annotations — it's the type used by db.get(), db.update(), db.delete(), etc. for their key parameters.
Enum keys¶
IntEnum and StrEnum subclasses can be used as HashKey or RangeKey field types. IntEnum values are stored as DynamoDB Number (N) attributes, and StrEnum values are stored as String (S) attributes.
from enum import StrEnum
from aiodynamodb import DynamoModel, HashKey, table
class AccountStatus(StrEnum):
active = "active"
disabled = "disabled"
@table("accounts")
class Account(DynamoModel):
status: HashKey[AccountStatus]
name: str
await db.put(Account(status=AccountStatus.active, name="Alice"))
account = await db.get(Account, hash_key=AccountStatus.active)
ReturnValues¶
ReturnValues is a literal type for the return_values parameter of db.update():
float and Decimal¶
DynamoDB requires Decimal for numeric values, not Python float. aiodynamodb handles this automatically:
- At
@table()decoration time, the decorator inspects the model's fields and sets_has_float_fields = Trueif any field (recursively, including nested models) has afloatannotation. - When serializing with
to_dynamo_compatible(), if_has_float_fieldsisTrue, allfloatvalues are recursively cast toDecimal(str(value))before being handed to boto3. - If no float fields are present, this traversal is skipped entirely for performance.
This means you can use float naturally in your models without thinking about Decimal: