DocsHub
Mongoose

Validation

Learn how Mongoose validates documents using built-in validators, custom validators, and how to handle validation errors.

Validation

Validation is one of the biggest reasons to use Mongoose. It ensures every document that gets saved follows the rules you define in your schema — before it ever reaches MongoDB.

This file covers built-in validators, custom validators, and how to handle validation errors properly.


When Validation Runs

Mongoose runs validation automatically:

  • When you call .save() on a document
  • When you call Model.create()

Mongoose does not run validation on:

  • updateOne(), updateMany(), findOneAndUpdate() — unless you explicitly enable it
  • insertMany() — unless you explicitly enable it
// Validation runs automatically
await Student.create({ name: "Ali Hassan", age: 16, grade: "10th" });

// Validation does NOT run by default
await Student.updateOne({ name: "Ali Hassan" }, { $set: { age: -5 } }); // succeeds even though age: -5 violates min: 5

Enabling validation on updates

Pass runValidators: true:

await Student.updateOne(
  { name: "Ali Hassan" },
  { $set: { age: -5 } },
  { runValidators: true }  // now this will throw a validation error
);

await Student.findOneAndUpdate(
  { name: "Ali Hassan" },
  { $set: { age: 30 } },
  { runValidators: true, new: true }
);

Always pass runValidators: true when using update methods if your schema has validation rules. Without it, updates can silently bypass all your validation — one of the most common Mongoose mistakes.


Built-in Validators

required

The field must be present and not null or undefined:

const studentSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true
  }
});

With a custom error message:

name: {
  type: String,
  required: [true, "Student name is required"]
}

min and max — for numbers

age: {
  type: Number,
  min: 5,
  max: 25
}

With custom messages:

age: {
  type: Number,
  min: [5, "Age must be at least 5"],
  max: [25, "Age cannot exceed 25"]
}

minlength and maxlength — for strings

name: {
  type: String,
  minlength: 2,
  maxlength: 100
}

With custom messages:

name: {
  type: String,
  minlength: [2, "Name must be at least 2 characters"],
  maxlength: [100, "Name cannot exceed 100 characters"]
}

enum — for restricted values

grade: {
  type: String,
  enum: ["9th", "10th", "11th", "12th"]
}

With a custom message:

grade: {
  type: String,
  enum: {
    values: ["9th", "10th", "11th", "12th"],
    message: "{VALUE} is not a valid grade"
  }
}

{VALUE} is automatically replaced with the invalid value in the error message.

match — regex pattern for strings

email: {
  type: String,
  match: [/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, "Please enter a valid email"]
}
phone: {
  type: String,
  match: [/^03\d{2}-\d{7}$/, "Phone must be in format 03XX-XXXXXXX"]
}

Custom Validators

For rules that built-in validators cannot express, write a custom validator function:

const studentSchema = new mongoose.Schema({
  name: String,
  age: Number,
  enrollmentDate: {
    type: Date,
    validate: {
      validator: function (value) {
        return value <= new Date();  // enrollment date cannot be in the future
      },
      message: "Enrollment date cannot be in the future"
    }
  }
});

The validator function receives the field's value and returns true (valid) or false (invalid).

Custom validator with access to other fields

Use a regular function (not an arrow function) to access this — the document being validated:

const studentSchema = new mongoose.Schema({
  age: Number,
  grade: {
    type: String,
    enum: ["9th", "10th", "11th", "12th"],
    validate: {
      validator: function (value) {
        // 9th grade students must be 13-15 years old
        if (value === "9th") {
          return this.age >= 13 && this.age <= 15;
        }
        return true; // no constraint for other grades
      },
      message: "9th grade students must be between 13 and 15 years old"
    }
  }
});

Arrow functions do not have their own this. If you need to access other fields in the document inside a custom validator, always use a regular function keyword — not an arrow function.

Async custom validators

Custom validators can be async — useful for checking against the database:

const courseSchema = new mongoose.Schema({
  code: {
    type: String,
    required: true,
    validate: {
      validator: async function (value) {
        // Check if a course with this code already exists
        const existing = await mongoose.models.Course.findOne({ code: value });

        // If editing an existing document, allow its own code
        if (existing && !existing._id.equals(this._id)) {
          return false; // duplicate found
        }
        return true;
      },
      message: "Course code already exists"
    }
  }
});

Validating Nested Objects

const studentSchema = new mongoose.Schema({
  name: { type: String, required: true },
  guardian: {
    name: { type: String, required: true },
    phone: {
      type: String,
      required: true,
      match: [/^03\d{2}-\d{7}$/, "Invalid phone format"]
    },
    relation: {
      type: String,
      enum: ["Father", "Mother", "Brother", "Sister", "Uncle", "Aunt", "Other"],
      required: true
    }
  }
});

Validating Array Elements

const studentSchema = new mongoose.Schema({
  name: String,
  grades: [
    {
      subject: { type: String, required: true },
      score: {
        type: Number,
        required: true,
        min: [0, "Score cannot be negative"],
        max: [100, "Score cannot exceed 100"]
      },
      semester: {
        type: Number,
        enum: [1, 2]
      }
    }
  ],
  subjects: {
    type: [String],
    validate: {
      validator: function (arr) {
        return arr.length > 0 && arr.length <= 10;
      },
      message: "A student must have between 1 and 10 subjects"
    }
  }
});

Handling Validation Errors

When validation fails, Mongoose throws a ValidationError. It contains an errors object with details about each field that failed.

try {
  await Student.create({
    name: "Al",          // too short — minlength is probably higher
    age: 30,             // exceeds max: 25
    grade: "13th",       // not in enum
    enrollmentDate: new Date("2030-01-01") // future date
  });
} catch (error) {
  if (error.name === "ValidationError") {
    // Loop through each field error
    for (const field in error.errors) {
      console.log(`${field}: ${error.errors[field].message}`);
    }
  }
}

Output:

age: Age cannot exceed 25
grade: 13th is not a valid grade
enrollmentDate: Enrollment date cannot be in the future

Sending validation errors in an API response

app.post('/students', async (req, res) => {
  try {
    const student = await Student.create(req.body);
    res.status(201).json(student);

  } catch (error) {
    if (error.name === "ValidationError") {
      const errors = {};
      for (const field in error.errors) {
        errors[field] = error.errors[field].message;
      }
      return res.status(400).json({ errors });
    }

    res.status(500).json({ message: "Server error" });
  }
});

Response when validation fails:

{
  "errors": {
    "age": "Age cannot exceed 25",
    "grade": "13th is not a valid grade"
  }
}

Validating Manually

You can run validation on a document without saving it — useful for checking data before committing to a save:

const student = new Student({
  name: "Ali Hassan",
  age: 30,
  grade: "10th"
});

const error = student.validateSync();

if (error) {
  console.log(error.errors.age.message); // "Age cannot exceed 25"
} else {
  await student.save();
}

There is also an async version — validate():

try {
  await student.validate(); // throws if invalid
  await student.save();
} catch (error) {
  console.log("Validation failed:", error.message);
}

Complete Validated Student Schema

Here is our school system's Student schema with full validation:

const mongoose = require('mongoose');

const studentSchema = new mongoose.Schema(
  {
    name: {
      type: String,
      required: [true, "Name is required"],
      trim: true,
      minlength: [2, "Name must be at least 2 characters"],
      maxlength: [100, "Name cannot exceed 100 characters"]
    },
    age: {
      type: Number,
      required: [true, "Age is required"],
      min: [5, "Age must be at least 5"],
      max: [25, "Age cannot exceed 25"]
    },
    grade: {
      type: String,
      required: [true, "Grade is required"],
      enum: {
        values: ["9th", "10th", "11th", "12th"],
        message: "{VALUE} is not a valid grade"
      },
      validate: {
        validator: function (value) {
          if (value === "9th") {
            return this.age >= 13 && this.age <= 15;
          }
          return true;
        },
        message: "9th grade students must be between 13 and 15 years old"
      }
    },
    email: {
      type: String,
      required: [true, "Email is required"],
      unique: true,
      lowercase: true,
      match: [/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, "Please enter a valid email"]
    },
    enrolled: {
      type: Boolean,
      default: true
    },
    enrollmentDate: {
      type: Date,
      default: Date.now,
      validate: {
        validator: function (value) {
          return value <= new Date();
        },
        message: "Enrollment date cannot be in the future"
      }
    },
    guardian: {
      name: { type: String, required: true },
      phone: {
        type: String,
        required: true,
        match: [/^03\d{2}-\d{7}$/, "Phone must be in format 03XX-XXXXXXX"]
      },
      relation: {
        type: String,
        enum: ["Father", "Mother", "Brother", "Sister", "Uncle", "Aunt", "Other"],
        required: true
      }
    },
    subjects: {
      type: [String],
      validate: {
        validator: function (arr) {
          return arr.length > 0 && arr.length <= 10;
        },
        message: "A student must have between 1 and 10 subjects"
      }
    },
    grades: [
      {
        subject: { type: String, required: true },
        score: {
          type: Number,
          required: true,
          min: [0, "Score cannot be negative"],
          max: [100, "Score cannot exceed 100"]
        },
        semester: {
          type: Number,
          enum: [1, 2]
        }
      }
    ]
  },
  { timestamps: true }
);

module.exports = mongoose.model('Student', studentSchema);

Quick Reference

ValidatorApplies toExample
requiredAny typerequired: [true, "message"]
min / maxNumbermin: 5, max: 25
minlength / maxlengthStringminlength: 2, maxlength: 100
enumStringenum: ["9th", "10th"]
matchStringmatch: [/regex/, "message"]
validateAny typeCustom function
uniqueAny typeRequires unique index — not a validator, enforced at DB level

unique: true in a schema is not actually a validator — it creates a unique index in MongoDB, and duplicate inserts fail with a database error (E11000), not a ValidationError. Handle these two error types differently in your error handling — check error.name === "ValidationError" for schema validation, and error.code === 11000 for duplicate key errors.

On this page