# How to Define Custom Validation That Runs on Save in Mongoose

> Learn how to define custom validation in Mongoose schemas that runs automatically on save. Improve data integrity with this essential technique for MongoDB applications.

- Repository: [mongodb/mongo](https://github.com/mongodb/mongo)
- Tags: how-to-guide
- Published: 2026-02-19

---

**Use the `validate` option in your schema field definition to attach a validator function that Mongoose executes automatically during the save process.**

When creating a schema in Mongoose, you can enforce data integrity by defining custom validation that runs on save. This client-side validation layer catches errors before documents ever reach the MongoDB server, providing immediate feedback and reducing network overhead. According to the `mongodb/mongo` source code, while the server can enforce its own validation rules via JSON Schema validators defined in [`src/mongo/db/document_validation/validator.cpp`](https://github.com/mongodb/mongo/blob/main/src/mongo/db/document_validation/validator.cpp), Mongoose validators run first in the application layer.

## Understanding the Mongoose Validation Pipeline

Mongoose executes validation logic in a specific sequence during the save operation. Understanding this flow helps you place your custom validation logic in the correct hook or option.

### Pre-save Hooks vs Path Validators

The `pre('save')` middleware runs before any built-in validation occurs. While you can perform side-effects here, actual field validation should reside in the `validate` option so that errors surface as `ValidationError` objects with proper path attribution.

After pre-save hooks complete, Mongoose iterates through every path in the document and executes all validators attached to that path. This is where your custom validation that runs on save actually executes.

### Document-level Validation

For constraints that involve multiple fields (e.g., ensuring `endDate` is after `startDate`), use document-level validation. You can attach validators to specific paths that reference `this` to access other fields, or use the schema-level `validate` option introduced in Mongoose v6.

## Defining Field-Level Custom Validators

The most common approach for custom validation that runs on save is the `validate` option within a field definition. This accepts an object with a `validator` function and an optional `message` property.

### Synchronous Validators

For simple checks that don't require I/O, provide a synchronous function that returns `true` for valid values and `false` for invalid ones.

```javascript
const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true,
    minlength: 3,
    // Custom validation runs on every save
    validate: {
      validator: function (v) {
        // Disallow whitespace
        return /^\S+$/.test(v);
      },
      message: props => `${props.value} contains spaces – usernames must be a single word.`
    }
  }
});

```

### Asynchronous Validators

When validation requires database queries or external API calls, return a Promise or use the callback pattern (pre-v4 style). Mongoose will await the resolution before completing the save operation.

```javascript
const emailSchema = new mongoose.Schema({
  email: {
    type: String,
    required: true,
    validate: {
      // Return a promise – Mongoose will await it
      validator: async function (v) {
        // `this` is the document being saved
        const count = await this.constructor.countDocuments({ email: v });
        // `this.isNew` ensures we don't count the document itself on updates
        return this.isNew ? count === 0 : count <= 1;
      },
      message: 'Email address already in use.'
    }
  }
});

```

## Schema-Level and Document-Level Validation

For cross-field validation or complex business logic, attach validators at the schema level or use pre-validate hooks.

```javascript
const orderSchema = new mongoose.Schema({
  quantity: { type: Number, required: true, min: 1 },
  pricePerItem: { type: Number, required: true, min: 0 },
  totalPrice: { type: Number }
});

// Compute totalPrice before validation
orderSchema.pre('validate', function (next) {
  this.totalPrice = this.quantity * this.pricePerItem;
  next();
});

// Ensure totalPrice matches the calculation (extra safety)
orderSchema.path('totalPrice').validate(function (v) {
  return v === this.quantity * this.pricePerItem;
}, 'Total price does not match quantity * pricePerItem.');

```

Mongoose v6+ also supports a schema-level `validate` option for document-wide checks:

```javascript
const productSchema = new mongoose.Schema(
  {
    name: { type: String, required: true },
    sku:  { type: String, required: true }
  },
  {
    // Runs after all path validators; useful for cross-field checks
    validate: {
      validator: function (doc) {
        // Example: SKU must start with the first three letters of the name
        return doc.sku.startsWith(doc.name.slice(0, 3).toUpperCase());
      },
      message: props => `SKU "${props.sku}" does not match product name.`
    }
  }
);

```

## MongoDB Server-Side Validation Context

While Mongoose handles client-side validation, the MongoDB server itself can enforce document structure through JSON Schema validators defined at the collection level. According to the `mongodb/mongo` source code, these mechanisms operate independently:

- **[`src/mongo/db/catalog/coll_mod.cpp`](https://github.com/mongodb/mongo/blob/main/src/mongo/db/catalog/coll_mod.cpp)** – Implements the `collMod` command that attaches JSON Schema validators to collections.
- **[`src/mongo/db/document_validation/validator.cpp`](https://github.com/mongodb/mongo/blob/main/src/mongo/db/document_validation/validator.cpp)** – Core logic that evaluates documents against collection-level validators during insert and update operations.
- **[`src/mongo/db/commands/validate.cpp`](https://github.com/mongodb/mongo/blob/main/src/mongo/db/commands/validate.cpp)** – Handles the `validate` command for checking collection consistency.

Mongoose custom validation that runs on save executes before the document is transmitted to the server, providing immediate feedback. The server-side validator acts as a final safety net for writes that bypass Mongoose (e.g., direct shell updates or writes from other applications).

## Summary

- **Field-level validation** uses the `validate` option in schema definitions to run custom logic during save operations.
- **Synchronous validators** return boolean values, while **asynchronous validators** return Promises for I/O-dependent checks.
- **Document-level validation** handles cross-field constraints via `schema.path().validate()`, `pre('validate')` hooks, or the schema-level `validate` option (v6+).
- Mongoose validation runs client-side before documents reach the MongoDB server, which maintains its own validation layer in files like [`src/mongo/db/document_validation/validator.cpp`](https://github.com/mongodb/mongo/blob/main/src/mongo/db/document_validation/validator.cpp).

## Frequently Asked Questions

### How do I ensure custom validators run when using `findOneAndUpdate`?

By default, Mongoose does not run validators on update operations. To enforce custom validation that runs on save behavior during updates, pass the `{ runValidators: true }` option to `findOneAndUpdate`, `updateOne`, or `updateMany`. Without this flag, Mongoose skips validation for performance reasons.

### Can I access other fields within a custom validator function?

Yes. In field-level validators, `this` refers to the document being saved, allowing you to access other fields via `this.otherField`. For schema-level validators (v6+), the function receives the entire document as an argument. Alternatively, use `pre('validate')` middleware to compute derived values before validation runs.

### What is the difference between `pre('save')` and the `validate` option?

`pre('save')` middleware executes before validation occurs and is intended for side-effects like hashing passwords or updating timestamps. The `validate` option defines validators that run during the validation phase and properly populate `ValidationError` objects with path-specific error messages. Use `validate` for constraint checking; use `pre('save')` for data transformation.

### How do I skip validation for a specific save operation?

To bypass custom validation that runs on save for a single operation, call `doc.save({ validateBeforeSave: false })`. You can also disable validation globally for a schema by setting `schema.set('validateBeforeSave', false)`, though this is not recommended for production environments as it removes the client-side safety layer.