Schema Validation

Keep data clean with schema validation: define required fields, BSON types, ranges, patterns and allowed values with $jsonSchema, add rules to existing collections with collMod, choose validation levels and actions, and read the detailed errors MongoDB returns.

Lesson 16 of 43Intermediate18 min read

MongoDB's flexible schema is a great strength while an application is evolving, but in production, unchecked data causes real problems. A typo creates emial instead of email; a bug saves a price as a string; an import leaves out a required field. Each one becomes a document that breaks queries, reports or the next version of your code.

Schema validation lets the database itself enforce rules. You attach a validator to a collection, usually written in $jsonSchema, and MongoDB checks every insert and update against it. Documents that break the rules are rejected with a detailed error explaining which field failed and why. You keep the flexibility you want, such as optional fields and nested documents, while guaranteeing the essentials.

In this lesson you'll create collections with validators, require fields, restrict types, ranges, lengths, patterns and allowed values, validate nested objects and arrays, add rules to collections that already contain data, and choose how strictly to apply them while you migrate.

Creating a validated collection

MongoDB Shell
db.createCollection('users', {
  validator: {
    $jsonSchema: {
      bsonType: 'object',
      required: ['email', 'name', 'createdAt'],
      properties: {
        email: { bsonType: 'string', pattern: '^[^@\\s]+@[^@\\s]+$' },
        name: { bsonType: 'string', minLength: 1, maxLength: 100 },
        age: { bsonType: 'int', minimum: 13, maximum: 120 },
        role: { enum: ['customer', 'staff', 'admin'] },
        createdAt: { bsonType: 'date' }
      }
    }
  }
})
  • required lists fields that must be present.
  • bsonType sets the type: 'string', 'int', 'double', 'decimal', 'bool', 'date', 'objectId', 'array', 'object', or several, such as ['double', 'int'].
  • minimum, maximum, minLength, maxLength, pattern and enum restrict values.
  • Fields not mentioned are allowed, unless you set additionalProperties: false, which then requires _id to be listed too.

Nested objects and arrays

MongoDB Shell
address: {
  bsonType: 'object',
  required: ['city', 'postcode'],
  properties: { city: { bsonType: 'string' }, postcode: { bsonType: 'string' } }
},
items: {
  bsonType: 'array',
  minItems: 1,
  items: {
    bsonType: 'object',
    required: ['sku', 'qty'],
    properties: { qty: { bsonType: 'int', minimum: 1 } }
  }
}

Rules nest to any depth. For arrays, items describes every element, and minItems, maxItems and uniqueItems restrict the list itself.

Existing collections, levels and actions

Add or change rules on an existing collection with collMod:

MongoDB Shell
db.runCommand({ collMod: 'products', validator: { $jsonSchema: { /* ... */ } }, validationLevel: 'moderate' })
  • validationLevel: 'strict' (default) checks every insert and update.
  • validationLevel: 'moderate' checks inserts, and updates only to documents that already pass, so old invalid documents can still be edited while you clean them up.
  • validationAction: 'error' (default) rejects invalid writes; 'warn' allows them but writes a warning to the log, which is useful for testing new rules safely.
  • Find documents that don't match a schema with db.products.find({ $nor: [{ $jsonSchema: schema }] }).

Reading validation errors and bypassing rules

When a write breaks the rules, MongoDB rejects it with error code 121, "Document failed validation". The useful part is the error's errInfo, which lists every rule that failed and why:

MongoDB Shell
try {
  db.users.insertOne({ email: 'a@b.c', age: 5 })
} catch (e) {
  printjson(e.errInfo)
}
// details.schemaRulesNotSatisfied lists, among others:
//   propertyName: 'age', operatorName: 'minimum', consideredValue: 5
//   operatorName: 'required', missingProperties: [ 'name' ]

Log errInfo in your application when an insert fails unexpectedly. It points straight at the field and the rule, which is far quicker than comparing the document against the schema by eye.

To see a collection's current rules, read its options:

MongoDB Shell
db.getCollectionInfos({ name: 'users' })[0].options.validator

Some trusted jobs need to write documents that don't pass, such as restoring an old backup taken before the rules existed. Commands such as insert, update and aggregate with $out or $merge accept bypassDocumentValidation: true for this. It needs a special privilege, bypassDocumentValidation, which ordinary application users shouldn't have, so the rules still protect everyday writes.

MongoDB Data Modelling Syntax

Validation syntax

db.createCollection('c', { validator: { $jsonSchema: {
  bsonType: 'object',
  required: ['name', 'price'],
  properties: {
    name: { bsonType: 'string', minLength: 2 },
    price: { bsonType: ['double', 'int', 'decimal'], minimum: 0 },
    status: { enum: ['draft', 'live'] },
    tags: { bsonType: 'array', maxItems: 10, items: { bsonType: 'string' } }
  }
} } })
db.runCommand({ collMod: 'c', validator: { /* ... */ }, validationLevel: 'moderate', validationAction: 'warn' })
db.getCollectionInfos({ name: 'c' })[0].options.validator

MongoDB Data Modelling Summary

  • Validators attached to a collection check every insert and update.
  • $jsonSchema describes required fields, BSON types, ranges, lengths, patterns and allowed values.
  • Nested objects and arrays have their own properties, required lists and item rules.
  • collMod adds rules to existing collections; moderate level and warn action help migrations.
  • Invalid writes fail with a detailed error describing which rule was broken.

MongoDB Data Modelling Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Rejecting invalid documents

MongoDB Shell
db.createCollection('products', {
  validator: { $jsonSchema: {
    bsonType: 'object',
    required: ['name', 'price', 'status'],
    properties: {
      name: { bsonType: 'string', minLength: 2 },
      price: { bsonType: ['double', 'int'], minimum: 0 },
      status: { enum: ['draft', 'live', 'archived'] }
    }
  } }
})

const tryInsert = (doc) => {
  try {
    db.products.insertOne(doc)
    return 'saved'
  } catch (e) {
    return `rejected (${e.code})`
  }
}
tryInsert({ name: 'Mug', price: 8.5, status: 'live' })
tryInsert({ name: 'Lamp', price: '24', status: 'live' })
tryInsert({ name: 'Rug', price: 79 })
tryInsert({ name: 'Pen', price: 1.2, status: 'sold out' })
tryInsert({ name: 'Kettle', price: 30, status: 'draft', colour: 'red' })
db.products.countDocuments()
Output
{ ok: 1 }
saved
rejected (121)
rejected (121)
rejected (121)
saved
2
Line-by-line explanation
  1. Lines 1 to 11The collection is created with rules for name, price and status.
  2. Line 7price may be a double or an integer, but not negative.
  3. Line 8status must be one of the listed values.
  4. Line 22A string price breaks the type rule; error 121 means document validation failed.
  5. Line 23A missing required field is rejected.
  6. Line 24A value outside the enum is rejected.
  7. Line 25Extra fields like colour are allowed, because additionalProperties isn't set to false.
Example 2

Nested rules for orders

MongoDB Shell
db.createCollection('orders', {
  validator: { $jsonSchema: {
    bsonType: 'object',
    required: ['customerId', 'items'],
    properties: {
      items: {
        bsonType: 'array',
        minItems: 1,
        items: {
          bsonType: 'object',
          required: ['sku', 'qty'],
          properties: { sku: { bsonType: 'string' }, qty: { bsonType: 'int', minimum: 1 } }
        }
      }
    }
  } }
})

const attempts = [
  { customerId: 'c1', items: [{ sku: 'MUG', qty: NumberInt(2) }] },
  { customerId: 'c1', items: [] },
  { customerId: 'c1', items: [{ sku: 'MUG', qty: NumberInt(0) }] },
  { customerId: 'c1', items: [{ qty: NumberInt(1) }] }
]
attempts.map((doc) => {
  try { db.orders.insertOne(doc); return 'ok' } catch (e) { return 'invalid' }
}).join(', ')
Output
{ ok: 1 }
ok, invalid, invalid, invalid
Line-by-line explanation
  1. Lines 6 to 8items must be an array with at least one element.
  2. Lines 9 to 12Every element must be an object with a sku and a positive integer qty.
  3. Line 20NumberInt makes the int type explicit, which matters in drivers and scripts where a plain number might be sent as a double.
  4. Line 21An empty order is rejected by minItems.
  5. Lines 22 to 23A zero quantity and a missing sku break the rules for array elements.
Example 3

Adding rules to an existing collection

MongoDB Shell
db.customers.insertMany([
  { _id: 1, email: 'ada@example.com' },
  { _id: 2, email: 'not-an-email' },
  { _id: 3 }
])
const schema = { bsonType: 'object', required: ['email'], properties: { email: { bsonType: 'string', pattern: '@' } } }

db.customers.find({ $nor: [{ $jsonSchema: schema }] }, { _id: 1 })
db.runCommand({ collMod: 'customers', validator: { $jsonSchema: schema }, validationLevel: 'moderate' }).ok
db.customers.updateOne({ _id: 3 }, { $set: { name: 'Cy' } }).modifiedCount
try { db.customers.insertOne({ _id: 4, email: 'nope' }) } catch (e) { print(`insert rejected: ${e.code}`) }
Output
{ acknowledged: true, insertedIds: { '0': 1, '1': 2, '2': 3 } }
[ { _id: 2 }, { _id: 3 } ]
1
1
insert rejected: 121
Line-by-line explanation
  1. Line 8$jsonSchema in a query finds existing documents that would break the new rules.
  2. Line 9collMod adds the validator to the existing collection.
  3. Line 9The moderate level lets old invalid documents keep being updated while you fix them.
  4. Line 11New documents must follow the rules.

Common MongoDB Data Modelling Mistakes

Relying only on application checks

Don't
// validation lives only in the web app; an import script writes prices as strings
Do
db.runCommand({ collMod: 'products', validator: { $jsonSchema: { properties: { price: { bsonType: ['double', 'int', 'decimal'] } } } } })

additionalProperties false without _id

Don't
{ $jsonSchema: { additionalProperties: false, properties: { name: { bsonType: 'string' } } } }   // every insert fails: _id isn't listed
Do
{ $jsonSchema: { additionalProperties: false, properties: { _id: {}, name: { bsonType: 'string' } } } }

Switching on strict rules over dirty data

Don't
db.runCommand({ collMod: 'users', validator: v })                 // edits to old bad documents start failing
Do
db.runCommand({ collMod: 'users', validator: v, validationAction: 'warn' })   // watch the logs, fix data, then switch to error

MongoDB Data Modelling in Real Projects

Validation in practice

Teams add validators for core collections such as users, orders and payments, and keep them in version control alongside migrations. Atlas and Compass can generate a starting schema from existing data. During migrations, new rules start with the warn action and moderate level, data is cleaned with update pipelines, and then the rules are made strict, so bad data can't creep back in.

MongoDB Data Modelling FAQ

How do I see the validation rules on a MongoDB collection?

Run db.getCollectionInfos({ name: 'users' }) and look at options.validator; options.validationLevel and options.validationAction show how strictly they're applied. In MongoDB Compass, the Validation tab of a collection shows and edits the same rules. If there's no validator key, the collection accepts any document.

How do I remove schema validation from a collection?

Run collMod with an empty validator: db.runCommand({ collMod: 'users', validator: {} }). Existing documents aren't affected either way, because validation only checks writes. A gentler option is validationAction: 'warn', which keeps the rules but only logs violations.

Does schema validation check documents that are already in the collection?

No. Rules apply only when documents are inserted or updated, so old documents that break new rules stay as they are. Find them with db.users.find({ $nor: [{ $jsonSchema: schema }] }), fix them, and use validationLevel: 'moderate' in the meantime if they still need to be edited.

How do I allow both int and double for a number field in $jsonSchema?

List several types: { bsonType: ['int', 'double'] }, or use 'number', which accepts every numeric type: int, long, double and decimal. This matters because the same number can arrive as an int from one client and a double from another. For money, require 'decimal' instead, so approximate doubles are rejected.

Is MongoDB schema validation a replacement for Mongoose validation?

They do different jobs and work well together. Mongoose validation runs in your Node.js application and gives friendly error messages before a request reaches the database. Database validation protects the data from every client, including scripts, imports and other services that don't use your Mongoose models.

MongoDB Data Modelling Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the schema so email must always be present.

{ $jsonSchema: { : ['email'] } }
Multiple choiceeasy

What happens when an insert breaks a validator with the default settings?

Multiple choicemedium

Which keyword restricts a field to a list of allowed values?

Multiple choicemedium

What does validationLevel: 'moderate' do?

Multiple choicehard

How can you find existing documents that break a schema?

MongoDB Data Modelling Quiz

5 questions to check what you learned.

  1. Which keyword sets a field's type in $jsonSchema?

    • bsonType
    • type
    • fieldType
    • kind
  2. How do you add validation to a collection that already exists?

    • Drop and recreate it
    • With the collMod command
    • With createIndex
    • It's impossible
  3. What does validationAction: 'warn' do?

    • Rejects invalid writes
    • Deletes invalid documents
    • Allows invalid writes but logs a warning
    • Emails the admin
  4. Are fields not listed in properties allowed?

    • No, never
    • Yes, unless additionalProperties is false
    • Only strings
    • Only in nested documents
  5. Why validate in the database as well as in the application?

    • It protects data from every writer, including scripts, imports and other services
    • It's faster
    • Applications can't validate
    • It replaces indexes