How to Extend Type Validation¶
Field[T] annotations are validated at class definition time. Only Firestore-compatible types are accepted — invalid types raise TypeError immediately, not at query time.
Supported types¶
| Category | Types |
|---|---|
| Scalars | str, int, float, bool, bytes, datetime.datetime |
| Scalars with built-in handlers | Decimal, datetime.date, datetime.time |
| Firestore SDK | GeoPoint, DocumentReference |
| Containers | list[T], set[T], tuple[T, ...], dict[str, V] |
| Structured | Map, dataclasses, TypedDict |
| Enums | enum.Enum, IntEnum, StrEnum |
| Optional | T | None |
| Third-party | pydantic, attrs, msgspec (if installed) |
Built-in handlers¶
Firestore cannot store Decimal, datetime.date, or datetime.time natively. Cendry registers handlers for these types automatically — no manual registration needed.
| Python type | Stored as | Round-trip |
|---|---|---|
Decimal |
str |
Lossless |
datetime.date |
datetime at midnight UTC |
Exact |
datetime.time |
datetime on 1970-01-01 UTC |
Exact |
class Event(Model, collection="events"):
price: Field[Decimal] # stored as "123.45"
day: Field[datetime.date] # stored as 2024-06-15T00:00:00Z
start: Field[datetime.time] # stored as 1970-01-01T14:30:00Z
Same convention as NDB
datetime.date and datetime.time follow Google NDB's DateProperty and TimeProperty conventions.
Register a custom type with a handler¶
The handler tells Cendry how to convert your type to and from Firestore:
from cendry import register_type
register_type(Money,
serialize=lambda v: {"amount": v.amount, "currency": v.currency},
deserialize=lambda v: Money(amount=v["amount"], currency=v["currency"]),
)
deserialize is required
Since Cendry reads from Firestore, you must provide deserialize. serialize is optional — it defaults to identity (passthrough) until writes are implemented.
Full handler class¶
For complex behavior, subclass BaseTypeHandler:
from cendry import BaseTypeHandler, register_type
class MoneyHandler(BaseTypeHandler):
def serialize(self, value):
return {"amount": value.amount, "currency": value.currency}
def deserialize(self, value):
return Money(amount=value["amount"], currency=value["currency"])
register_type(Money, handler=MoneyHandler())
Predicate-based registration¶
For families of types:
Register a type without a handler¶
If you just need validation (the type is already handled by Firestore natively):
Per-context registry¶
Override the type registry for a specific context:
from cendry import TypeRegistry, Cendry
registry = TypeRegistry()
registry.register(MySpecialType, deserialize=lambda v: MySpecialType(v))
ctx = Cendry(type_registry=registry)
Invalid types¶
These raise TypeError at class definition:
class Bad(Model, collection="bad"):
val: Field[complex] # unsupported scalar
data: Field[dict[int, str]] # dict keys must be str
city: Field[OtherModel] # cannot nest Model
Container elements are validated too
Field[list[complex]] is also rejected — validation recurses into container types.