Skip to content

Fix: MongoDB Schema Validation Error — Document Failed Validation

FixDevs · (Updated: )

Part of:  JavaScript & TypeScript Errors

Quick Answer

How to fix MongoDB schema validation errors — $jsonSchema rules, required fields, type mismatches, enum constraints, bypassing validation for migrations, and Mongoose schema conflicts.

The Error

MongoDB rejects a document insert or update with:

MongoServerError: Document failed validation
Additional information: {
  failingDocumentId: ObjectId('...'),
  details: {
    operatorName: '$jsonSchema',
    schemaRulesNotSatisfied: [
      {
        operatorName: 'required',
        specifiedAs: { required: [ 'email', 'createdAt' ] },
        missingProperties: [ 'createdAt' ]
      }
    ]
  }
}

Or a type mismatch triggers the error:

schemaRulesNotSatisfied: [
  {
    operatorName: 'properties',
    propertiesNotSatisfied: [
      {
        propertyName: 'age',
        description: 'item did not match any allowed types',
        details: [ { operatorName: 'bsonType', specifiedAs: 'int', reason: 'type did not match', consideredValue: '25', consideredType: 'string' }]
      }
    ]
  }
]

Or validation works for new documents but fails during a migration or bulk update:

BulkWriteError: Document failed validation
writeErrors: [ { index: 14, code: 121, errmsg: 'Document failed validation' } ]

Why This Happens

MongoDB 3.6+ supports collection-level schema validation using JSON Schema ($jsonSchema). When enabled, every insert and update that affects the document is checked against the schema. This validation runs on the server, after the driver has already serialized the document — meaning even if your application-level validation passes, MongoDB’s check can still reject the write.

The validator rules are stored in the collection’s metadata and applied with two settings: validationLevel (which documents to check) and validationAction (whether to reject or warn). A common surprise is that adding a schema to an existing collection does not immediately fail — it only fails on subsequent writes. So a deploy that adds validation can appear successful for hours before the first write attempt against a non-conforming document fails.

BSON type strictness is the second major source of confusion. JavaScript has one numeric type, but BSON has at least four (int, long, double, decimal). A JavaScript number 5 is serialized as a BSON double by default. A schema rule that requires bsonType: 'int' rejects this document even though, from JavaScript’s perspective, 5 looks like an integer. Mongoose adds another layer by sometimes coercing strings to numbers before sending, which can make Mongoose-validated documents fail MongoDB-validated checks.

Other causes:

  • Enum constraint violated — a status field set to "archived" when the schema only allows ["active", "inactive"].
  • Existing documents out of sync — adding validation to a collection that already has documents not conforming to the new rules doesn’t fail immediately, but those documents fail on next update.
  • $set updates on partial documents — MongoDB validates the entire document after a $set update, not just the updated fields. If the existing document has a field that violates the schema, even a $set on an unrelated field fails.

In Production: Incident Lens

The schema validation error is one of the most disruptive MongoDB failures because, unlike network errors, it does not retry away. Every write that violates the schema fails consistently with code: 121. If a deploy adds or modifies a $jsonSchema validator and existing documents don’t conform, every update path through the affected collection breaks at once.

How it surfaces: A new feature requires a createdAt field, so the team adds required: ['email', 'createdAt'] to the user collection validator. The deploy goes out. Old user documents that predate this field continue to read fine, but the next time any of those users updates their profile (login refreshes a lastSeenAt, email change, password reset), MongoDB rejects the write because the full document still lacks createdAt. Error rates spike on users.updateOne() callsites. The API returns 500s on profile updates. Login flows that refresh session timestamps start failing.

Blast radius: All writes to the affected collection. This is the worst-case blast radius for a database change. Read operations continue to work, so the failure mode is “users can view but not modify,” which masquerades as a generic backend outage in user-facing error messages. If the collection is critical (users, sessions, orders), the entire application becomes effectively read-only.

Monitoring signals:

  • Spike in write error rate for the affected collection (MongoServerError with code: 121)
  • Sudden increase in 5xx responses on endpoints that perform updateOne, replaceOne, or findOneAndUpdate on the collection
  • MongoDB Atlas metrics: Document Validation Failures counter rising
  • Application logs filled with schemaRulesNotSatisfied stack traces

Recovery sequence: The fastest mitigation is to change validationAction from error to warn via db.runCommand({ collMod: 'users', validationAction: 'warn' }) — this immediately stops rejecting writes while logging them. Next, run a backfill migration to populate missing required fields in existing documents. Then switch back to validationAction: 'error'. If validationLevel: 'strict' was the issue, lowering to 'moderate' allows updates to non-conforming documents without checking them.

Postmortem preventives: Before adding any new required field or stricter type rule, run a count query for documents that would fail: db.collection.find({ $nor: [newSchema] }).count(). Deploy validator changes in two phases: first with validationAction: 'warn' to audit violations in production logs, then promote to error after the backfill. Test schema migrations against a production snapshot.

Fix 1: Read the Validation Error Details

MongoDB’s error message includes exactly which rules failed. Expand the details object to find the specific constraint:

// Node.js — catch and log the full error details
try {
  await db.collection('users').insertOne(doc);
} catch (err) {
  if (err.code === 121) {  // Document failed validation error code
    console.error('Validation failure details:');
    console.error(JSON.stringify(err.errInfo?.details, null, 2));
    // errInfo.details.schemaRulesNotSatisfied shows exactly what failed
  }
  throw err;
}

Inspect the current schema on a collection:

// Get the current validation rules
const collectionInfo = await db.listCollections({ name: 'users' }).toArray();
const validator = collectionInfo[0]?.options?.validator;
console.log(JSON.stringify(validator, null, 2));

// Or in MongoDB shell:
// db.getCollectionInfos({ name: 'users' })[0].options.validator

Check which existing documents violate a schema before applying it:

// Query documents that would fail validation before adding the schema
const schema = {
  $jsonSchema: {
    required: ['email', 'createdAt'],
    properties: {
      age: { bsonType: 'int' }
    }
  }
};

// Find documents that DON'T match the schema (will fail after schema is applied)
const violations = await db.collection('users').find({
  $nor: [schema]
}).toArray();

console.log(`${violations.length} documents would fail validation`);

Fix 2: Fix Type Mismatches

BSON types are stricter than JavaScript types. The most common mismatch is between int, double, and long:

// WRONG — JavaScript numbers are doubles by default
// If schema requires bsonType: 'int', this fails:
await db.collection('products').insertOne({
  name: 'Widget',
  quantity: 5,      // Stored as BSON double (64-bit float) — fails int validation
  price: 9.99       // This is fine as double
});

// CORRECT — use Int32 for integer fields
const { Int32 } = require('mongodb');

await db.collection('products').insertOne({
  name: 'Widget',
  quantity: new Int32(5),   // Explicit BSON int32
  price: 9.99               // double — fine for decimal values
});

// Or change the schema to accept 'number' (covers both int and double):
const validator = {
  $jsonSchema: {
    properties: {
      quantity: { bsonType: ['int', 'double'] },  // Accept both
      // or use 'number' to accept any numeric type:
      price: { bsonType: 'number' }
    }
  }
};

Common BSON type names:

JavaScript valueBSON type string
Integer (whole)"int" or "long"
Decimal / float"double" or "decimal"
"text""string"
true/false"bool"
new Date()"date"
null"null"
[]"array"
{}"object"
ObjectId"objectId"

Allow null or missing optional fields:

// Schema that allows email to be either a string or null (optional field)
const validator = {
  $jsonSchema: {
    properties: {
      phone: {
        bsonType: ['string', 'null'],   // Can be string or null
        description: 'Phone number, optional'
      }
    }
  }
};

Fix 3: Add Required Fields to Existing Documents

When adding a required constraint to a field that some existing documents don’t have, fix the existing documents first:

// Step 1: Find documents missing the required field
const missing = await db.collection('users').find({
  createdAt: { $exists: false }
}).toArray();

console.log(`${missing.length} users missing createdAt`);

// Step 2: Backfill the field before adding validation
if (missing.length > 0) {
  await db.collection('users').updateMany(
    { createdAt: { $exists: false } },
    { $set: { createdAt: new Date('2024-01-01') } }  // Reasonable default
  );
}

// Step 3: Now safe to add the validation schema
await db.command({
  collMod: 'users',
  validator: {
    $jsonSchema: {
      bsonType: 'object',
      required: ['email', 'createdAt'],
      properties: {
        email: { bsonType: 'string' },
        createdAt: { bsonType: 'date' }
      }
    }
  },
  validationLevel: 'strict',    // Enforce on all inserts and updates
  validationAction: 'error'     // Reject violating documents (default)
});

Fix 4: Use validationLevel for Migrations

During migrations, temporarily relax validation to avoid blocking updates:

// Set validation to 'moderate' — only validate NEW documents and documents
// that already pass the current schema. Existing non-conforming documents
// can still be updated without validation.
await db.command({
  collMod: 'users',
  validationLevel: 'moderate'
});

// Perform migration
await db.collection('users').updateMany({}, {
  $set: { status: 'active' }
});

// Re-enable strict validation after migration
await db.command({
  collMod: 'users',
  validationLevel: 'strict'
});

Validation levels:

  • "strict" (default) — validate all inserts and updates
  • "moderate" — validate inserts and updates to documents that already pass the current schema; existing non-conforming documents can be updated without validation
  • "off" — no validation (use only for emergency recovery)

Validation actions:

  • "error" (default) — reject documents that fail validation
  • "warn" — allow the write but log a warning (useful for auditing before enforcing)

Set validationAction: "warn" when first adding a schema to audit violations without breaking the application:

await db.command({
  collMod: 'users',
  validator: { $jsonSchema: { ... } },
  validationAction: 'warn'    // Log but don't reject — check logs for violations
});

Fix 5: Align Mongoose Schema with MongoDB Validation

Mongoose and MongoDB-level validation operate independently. A Mongoose schema doesn’t automatically create MongoDB collection validators:

// Mongoose schema — validates BEFORE sending to MongoDB
const userSchema = new mongoose.Schema({
  email: { type: String, required: true },
  age: { type: Number, min: 0 }
});

// This does NOT create a MongoDB $jsonSchema validator
// MongoDB-level validation must be created separately

// Option 1: Use ONLY Mongoose validation (no MongoDB-level schema)
// This is the simpler approach for most applications

// Option 2: Add MongoDB validation explicitly (for data integrity beyond Mongoose)
const validationSchema = {
  $jsonSchema: {
    bsonType: 'object',
    required: ['email'],
    properties: {
      email: { bsonType: 'string' },
      age: { bsonType: ['int', 'double', 'null'] }
    }
  }
};

// Apply during application startup
await mongoose.connection.db.command({
  collMod: 'users',
  validator: validationSchema
});

When both Mongoose and MongoDB validation are active, MongoDB validation fires after Mongoose sends the document. A document that passes Mongoose validation can still fail MongoDB validation if the types differ (e.g., Mongoose coerces strings to numbers, but MongoDB receives the number type, which may not match a strict bsonType: 'string' rule).

Common Mistake: Using mongoose.Schema.Types.ObjectId for a reference field in Mongoose, then specifying bsonType: 'string' in the MongoDB schema. Mongoose stores ObjectId references as BSON ObjectIds, not strings.

Fix 6: Handle Enum Constraints

When a field has an enum constraint, every insert and update must use one of the allowed values:

// Schema with enum constraint
const validator = {
  $jsonSchema: {
    properties: {
      status: {
        bsonType: 'string',
        enum: ['pending', 'active', 'inactive', 'deleted'],
        description: 'Must be one of the allowed status values'
      }
    }
  }
};

// WRONG — 'archived' not in the enum list
await db.collection('users').updateOne(
  { _id: userId },
  { $set: { status: 'archived' } }   // Fails: 'archived' not allowed
);

// CORRECT — use an allowed value, or update the schema to include 'archived'
await db.collection('users').updateOne(
  { _id: userId },
  { $set: { status: 'inactive' } }   // 'inactive' is in the enum list
);

// To add 'archived' to the enum — update the schema:
await db.command({
  collMod: 'users',
  validator: {
    $jsonSchema: {
      properties: {
        status: {
          bsonType: 'string',
          enum: ['pending', 'active', 'inactive', 'deleted', 'archived']  // Added
        }
      }
    }
  }
});

Fix 7: Bypass Validation for Emergency Writes

In emergencies (data recovery, critical hotfix), you can bypass validation using the bypassDocumentValidation option:

// Insert bypassing validation — use sparingly
await db.collection('users').insertOne(
  { _id: new ObjectId(), partialData: true },   // Would fail validation
  { bypassDocumentValidation: true }
);

// In a transaction
const session = client.startSession();
await session.withTransaction(async () => {
  await db.collection('users').updateMany(
    { status: { $exists: false } },
    { $set: { status: 'pending' } },
    { session, bypassDocumentValidation: true }
  );
});

Warning: bypassDocumentValidation requires the bypassDocumentValidation privilege in MongoDB’s access control. Don’t use this as a workaround for everyday writes — fix the root cause instead.

Still Not Working?

Validation on nested fields$jsonSchema supports nested objects with properties inside properties. If a nested document fails validation, the error message shows the full property path.

Array item validation — to validate each item in an array, use items:

{
  $jsonSchema: {
    properties: {
      tags: {
        bsonType: 'array',
        items: { bsonType: 'string' },  // Each tag must be a string
        maxItems: 10
      }
    }
  }
}

$set only touches specified fields, but validation runs on the full document — if your full document has fields that violate the schema, a $set targeting only valid fields still triggers a validation failure on the entire document.

Mongo Atlas vs self-hosted differences — Atlas enforces validation strictly and doesn’t allow bypassDocumentValidation at the driver level by default. Check your Atlas cluster’s security settings if bypass doesn’t work.

Schema changes in Mongock/migrations — if you use a migration tool, schema changes applied to validators must account for the migration’s write operations. Apply validationLevel: 'moderate' before the migration step, then restore strict after.

additionalProperties: false rejecting unknown fields — if your schema has additionalProperties: false, MongoDB rejects any document containing fields not explicitly listed in properties. A client sending a new field (e.g., a feature flag toggled on) will see writes fail until the schema is updated. Either remove additionalProperties: false for forward compatibility or version your schema and clients together.

Upsert with $setOnInsert and validation — an upsert that creates a document via $setOnInsert builds the document from the combined operators and the filter. If the resulting document is missing a required field, validation rejects the upsert with no clear hint about which operator was responsible. Print the projected document with a dry-run before issuing the upsert.

Aggregation pipeline $merge and $out$merge respects validation rules; $out does too in MongoDB 5.0+. A pipeline that worked in dev (where the target collection had no validator) fails in production where it does. Test pipelines against the production validator schema.

For related MongoDB issues, see Fix: MongoDB Aggregation Not Working, Fix: MongoDB Connection Refused, Fix: MongoDB Duplicate Key Error, and Fix: Mongoose Validation Failed.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles