Schema Validation
Validation runs automatically before every write, using a Zod schema you attach at construction.
Validation happens automatically before any write operation using Zod schemas. Attach a schema with
FirestoreRepository.withSchema(...) and the repository derives the write and update schemas it
enforces on create, update, and patch.
const userSchema = z.object({ id: z.string(), name: z.string().min(1), email: z.string().email(), age: z.number().int().positive().optional(),});
const userRepo = FirestoreRepository.withSchema<User>(db, 'users', userSchema);
try { await userRepo.create({ name: '', email: 'not-an-email', age: -5, });} catch (error) { if (error instanceof ValidationError) { console.log(error.issues); // [ // { path: ['name'], message: 'Too small: expected string to have >=1 characters' }, // { path: ['email'], message: 'Invalid email address' }, // { path: ['age'], message: 'Too small: expected number to be >0' } // ] }}The message text on each issue is produced by Zod, so the exact wording depends on your Zod
version (this package supports both Zod 3 and Zod 4) and any custom messages you pass to your schema
(e.g. z.string().min(1, 'Name is required')). The path array is what you should branch on.
Required top-level id
Section titled “Required top-level id”Every schema you pass to withSchema(...) (and to a subcollection with a schema) must declare a
required top-level id: z.string(). The repository asserts this at construction and throws if the
id field is missing. This is the read shape — it does not force id onto write inputs.
Validation behavior
Section titled “Validation behavior”- Include a required top-level
idfield in schemas passed towithSchema(...). create()validates against an internal write schema derived fromschema.omit({ id: true }).update()validates against an internal update schema derived fromschema.omit({ id: true }).partial().- Top-level
idis ignored/stripped fromcreate/update/patchpayloads before validation and writes. create()therefore does not requireidin its input type — the id is auto-generated (or, forupsert, taken from the explicitidargument); reads always includeid.- Only the document-level top-level
idis stripped; nested IDs (for exampleitems[].id) are treated as normal domain data. - Write operations follow this sequence:
before*hook -> validation -> Firestore write ->after*hook. - Validation errors are thrown after
before*hooks run and before any Firestore write occurs. - Firestore
FieldValuesentinels are supported in write payloads. By default (sentinelPolicy: 'permissive') any sentinel is accepted on any field — sentinel-valued paths are skipped during schema validation while non-sentinel paths are still validated. To enforce which sentinels a field may receive, declare them with the per-field combinators and opt intosentinelPolicy: 'strict'(see Per-Field Sentinel Approval).
Where
idlives (and why the curried form doesn’t change it). There are three separateidcontexts, and it’s easy to conflate them:
- In the schema — a required top-level
id(e.g.id: z.string()) is required; the repository throws at construction otherwise. It describes the read shape.- On write inputs (
create/update/upsert/patch) —idis never required and is always stripped. The document id comes from Firestore (auto-generated oncreate) or from the method’sidargument (update(id, …),upsert(id, …)).- On reads —
idis always present (results are typedT & { id }).The curried form (
withSchema<T>()(…)) changes only the write value types of non-idfields (W = z.infer<schema>, enabling cast-free combinator writes). All threeidrules above are identical in the direct and curried forms.
Accessing derived schemas
Section titled “Accessing derived schemas”The repository exposes the read schema you provided plus the two schemas it derives internally for validation.
const userRepo = FirestoreRepository.withSchema<User>(db, 'users', userSchema);
// Canonical read schema (includes required id)const readSchema = userRepo.schemas?.read;
// Internal write schemas used by repository validationconst createSchema = userRepo.schemas?.create; // userSchema without idconst updateSchema = userRepo.schemas?.update; // create schema made partial