Skip to content

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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
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:

typescript
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:

typescript
// 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:

typescript
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:

typescript
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:

typescript
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.

Released under the MIT License.