Validation
Monch validates documents at multiple points to catch errors early and ensure data integrity.
Insert Validation
Full Zod schema validation on insertOne and insertMany:
// Schema
const Users = collection({
name: 'users',
schema: {
_id: field.id(),
name: field.string().min(1),
email: field.email(),
age: field.number().min(0).optional(),
},
});
// Valid insert
const user = await Users.insertOne({
name: 'Alice',
email: 'alice@example.com',
});
// Invalid insert - throws MonchValidationError
await Users.insertOne({
name: '', // Too short
email: 'invalid', // Not an email
});Update Validation
Partial validation for $set and $setOnInsert operations:
// Only the email field is validated
await Users.updateOne(
{ _id: user._id },
{ $set: { email: 'invalid-email' } } // Throws MonchValidationError
);
// Valid update
await Users.updateOne(
{ _id: user._id },
{ $set: { email: 'alice@newdomain.com' } } // Works
);Partial Validation
Only fields in $set and $setOnInsert are validated. Other update operators like $inc, $push, $unset are not validated.
Upsert Validation
Monch uses different validation strategies for upserts depending on the operation type:
updateOne / findOneAndUpdate (Partial Validation)
Uses the same partial validation as regular updates - only validates fields in $set and $setOnInsert:
// This works - only validates the 'role' field being set
await Users.updateOne(
{ email: 'new@example.com' },
{ $set: { role: 'admin' } },
{ upsert: true }
);
// Invalid field values are still rejected
await Users.updateOne(
{ email: 'new@example.com' },
{ $set: { age: -5 } }, // Throws if age has min(0) constraint
{ upsert: true }
);MongoDB Responsibility
With partial validation, MongoDB may create documents with missing required fields. This is intentional - updateOne upserts are designed for incremental updates where the document may be created with minimal fields and filled in later.
replaceOne / findOneAndReplace (Full Validation)
Uses full document validation since you're replacing the entire document:
// This will throw because 'name' is required
await Users.replaceOne(
{ email: 'new@example.com' },
{ email: 'new@example.com', role: 'admin' },
{ upsert: true }
);
// Throws: "name" is required
// Valid upsert - all required fields provided
await Users.replaceOne(
{ email: 'new@example.com' },
{ email: 'new@example.com', name: 'New User', role: 'admin' },
{ upsert: true }
);Replace Validation
Full validation on replaceOne and findOneAndReplace:
// Full document validation
await Users.replaceOne(
{ _id: user._id },
{ name: 'Alice', email: 'alice@example.com' }
);The replacement document must satisfy the entire schema.
Bulk Write Validation
Each operation in bulkWrite is validated according to its type:
await Users.bulkWrite([
// Full validation
{ insertOne: { document: { name: 'Bob', email: 'bob@example.com' } } },
// Partial validation
{ updateOne: { filter: { _id: id1 }, update: { $set: { name: 'Robert' } } } },
// Full validation
{ replaceOne: { filter: { _id: id2 }, replacement: { name: 'Carol', email: 'carol@example.com' } } },
]);No Hooks
bulkWrite uses Zod-only validation and does not trigger any lifecycle hooks (beforeValidate, afterValidate, beforeInsert, etc.).
Error Handling
Validation errors throw MonchValidationError:
import { MonchValidationError } from '@codician-team/monch';
try {
await Users.insertOne({ name: '', email: 'invalid' });
} catch (error) {
if (error instanceof MonchValidationError) {
// Raw Zod issues
console.log(error.issues);
// [
// { path: ['name'], message: 'String must contain at least 1 character(s)', code: 'too_small' },
// { path: ['email'], message: 'Invalid email', code: 'invalid_string' }
// ]
// Human-readable format
console.log(error.formatIssues());
// - name: String must contain at least 1 character(s)
// - email: Invalid email
// Form-friendly errors
console.log(error.toFormErrors());
// { name: 'String must contain at least 1 character(s)', email: 'Invalid email' }
// Quick access
console.log(error.firstError); // 'String must contain at least 1 character(s)'
console.log(error.firstField); // 'name'
}
}The _root Key
When toFormErrors() or firstField encounter top-level validation errors (errors with empty path), they use _root as the key:
// Schema with top-level refinement
const schema = z.object({
password: z.string(),
confirmPassword: z.string(),
}).refine(
(data) => data.password === data.confirmPassword,
{ message: 'Passwords must match' }
);
// If refinement fails:
error.toFormErrors(); // { _root: 'Passwords must match' }
error.firstField; // '_root'Validation with Hooks
Validation hooks let you transform data before and after Zod validation:
const Posts = collection({
name: 'posts',
schema: {
_id: field.id(),
title: field.string(),
slug: field.string(),
},
hooks: {
// Transform BEFORE Zod validation
beforeValidate: (doc) => ({
...doc,
slug: doc.title.toLowerCase().replace(/\s+/g, '-'),
}),
// Transform AFTER Zod validation (has access to defaults)
afterValidate: (doc) => ({
...doc,
searchIndex: `${doc.title} ${doc.slug}`.toLowerCase(),
}),
},
});See Lifecycle Hooks for more details.
Manual Validation
You can validate data manually using the runtime schema:
const Users = collection({
name: 'users',
schema: {
_id: field.id(),
name: field.string(),
email: field.email(),
},
});
// Access runtime schema
const schema = Users.$schema;
// Safe parse (returns result object)
const result = schema.safeParse({ name: 'Alice', email: 'invalid' });
if (!result.success) {
console.log(result.error.issues);
}
// Parse (throws on error)
try {
const data = schema.parse({ name: 'Alice', email: 'alice@example.com' });
} catch (error) {
// ZodError
}Bypassing Validation
In rare cases where you need to bypass validation, you can access the raw MongoDB collection:
const raw = await Users.collection;
await raw.insertOne({ /* unvalidated data */ });Use With Caution
Bypassing validation can lead to invalid data in your database. Only use this for migrations, bulk imports, or other special cases where you've validated data externally.