Skip to content

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.

city = ctx.get(City, "SF")
print(city.id)  # "SF"

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:

class City(Model, collection="cities"):
    name: Field[str] = field(alias="displayName")

See How to Use Aliases for details.

Restrictions

What you can't do

  • Model inside ModelField[OtherModel] raises TypeError. Use Map for embedded data.
  • Unsupported typesField[complex] and other non-Firestore types raise TypeError at class definition.
  • Dict keys must be strField[dict[int, str]] is rejected.