How to Define Models¶
Basic model¶
Every model needs a collection= — Cendry never guesses collection names.
from cendry import Model, Field
class City(Model, collection="cities"):
name: Field[str]
state: Field[str]
population: Field[int]
Optional fields with defaults¶
Use field() for defaults. Fields without defaults are required.
from cendry import field
class City(Model, collection="cities"):
name: Field[str]
nickname: Field[str | None] = field(default=None)
tags: Field[list[str]] = field(default_factory=list)
Embedded maps¶
Use Map for nested data — Firestore maps within a document.
from cendry import Map
class Address(Map):
street: Field[str]
city: Field[str]
class User(Model, collection="users"):
name: Field[str]
address: Field[Address]
Maps can nest other Maps. They have no collection and no id.
Info
When Firestore returns {"address": {"street": "123 Main", "city": "SF"}}, the address field is automatically deserialized into an Address instance.
Inheritance and mixins¶
Share fields across models using Map as a mixin:
from datetime import datetime
class TimestampMixin(Map):
created_at: Field[datetime]
updated_at: Field[datetime]
class City(TimestampMixin, Model, collection="cities"):
name: Field[str]
All inherited fields work with filters, ordering, and serialization.
Document ID¶
Every Model has id: str | None, defaulting to None. Firestore Native mode only supports string document IDs.
Key constraints:
| Constraint | Detail |
|---|---|
| Max size | 1,500 bytes |
| Forbidden values | . and .. |
| Forbidden characters | / (forward slash) |
| Avoid | Monotonically increasing IDs (causes write hotspots) |
Datastore mode supports integer IDs
Firestore in Datastore mode allows both string and 64-bit integer IDs. Cendry targets Native mode, so id is always str | None. See Comparisons for details.
See Work with Document IDs for auto-generation, manual IDs, and best practices.
Enum fields¶
Enums are supported and auto-converted:
import enum
class Status(enum.Enum):
ACTIVE = "active"
INACTIVE = "inactive"
class User(Model, collection="users"):
status: Field[Status] # stores by value: "active"
role: Field[Role] = field(enum_by="name") # stores by name: "ADMIN"
Tip
IntEnum and StrEnum also work. The enum_by setting controls whether the value or the name is stored in Firestore.
Field aliases¶
When the Firestore field name differs from the Python name:
See How to Use Aliases for details.
Restrictions¶
What you can't do
- Model inside Model —
Field[OtherModel]raisesTypeError. UseMapfor embedded data. - Unsupported types —
Field[complex]and other non-Firestore types raiseTypeErrorat class definition. - Dict keys must be
str—Field[dict[int, str]]is rejected.