Using Field Projections for Efficient Firestore Reads
Use plain Pydantic models as field masks to fetch only the columns you need, reducing Firestore bandwidth and keeping response payloads lean.
By default, every query fetches the complete document from Firestore — all fields, regardless of whether your application actually uses them. Projections let you tell Firestore exactly which fields to return, transferring only the data you need. In Firestore Pydantic ODM, a projection is a plain pydantic.BaseModel class whose field names define the mask. You pass it to find() or find_one() and the returned instances are of that projection type, not the original model type.
When you provide a projection class to find() or find_one(), the ODM inspects the class’s field definitions and calls Firestore’s .select() API with only those field names. Firestore’s server then strips all other fields before sending the response over the wire.Each document yielded by find() is constructed as an instance of your projection class, not the original model. This means you get a lean, validated object with only the fields you requested.
from pydantic import BaseModelfrom firestore_pydantic_odm import BaseFirestoreModelclass User(BaseFirestoreModel): class Settings: name = "users" name: str email: str age: int = 0# Projection: only the name fieldclass UserNameProjection(BaseModel): name: strasync for user in User.find(projection=UserNameProjection): print(type(user)) # <class 'UserNameProjection'> print(user.name) # ✅ available # user.email # ❌ AttributeError — not in projection
Firestore bills per document read, not per byte transferred. However, projections reduce the payload size of each response, which decreases network latency and egress costs — especially important for large documents or high-throughput services.
A projection model is a regular pydantic.BaseModel (not a BaseFirestoreModel) with one field per Firestore attribute you want to retrieve. Field names must match the Firestore document field names exactly, or you can use Pydantic’s alias= to map a different Python name to the underlying Firestore field.
from pydantic import BaseModel, Field# Simple projection — fetch only name and emailclass UserSummary(BaseModel): name: str email: str# Projection with alias — Firestore stores "createdAt", Python uses "created_at"class UserTimestamp(BaseModel): name: str created_at: str = Field(alias="createdAt")
When a field in your projection has an alias, the ODM uses the alias as the Firestore field name in the field mask. This ensures the .select() call requests the correct Firestore column even if your Python attribute has a different name.
The yielded or returned objects are instances of the projection class, not the original model. Keep this in mind for type annotations and downstream logic:
from typing import AsyncGeneratorfrom pydantic import BaseModelclass UserSummary(BaseModel): name: str email: strasync def fetch_summaries() -> list[UserSummary]: results: list[UserSummary] = [] async for summary in User.find(projection=UserSummary): results.append(summary) return results
Because projection instances are plain BaseModel objects, they do not have Firestore methods like .save(), .update(), or .delete(). If you need to modify a document after fetching its projection, use User.get(summary.id) to load the full model instance first — but only if you include id in your projection.
The ODM always populates the id field from doc.id when constructing instances. To access the document ID on a projection, simply add id: Optional[str] = None to your projection class:
from typing import Optionalfrom pydantic import BaseModelclass UserWithId(BaseModel): id: Optional[str] = None name: strasync for user in User.find(projection=UserWithId): print(user.id, user.name) # Use the ID to load the full model when needed full_user = await User.get(user.id)
When your model stores Firestore field names that differ from your Python attribute names (using Pydantic’s alias=), your projection must use the same alias to ensure the field mask is correct:
from pydantic import BaseModel, Field# Firestore document stores: { "firstName": "Alice", "lastName": "Smith" }class FullNameProjection(BaseModel): first_name: str = Field(alias="firstName") last_name: str = Field(alias="lastName")async for user in User.find(projection=FullNameProjection): print(user.first_name, user.last_name)