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
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' }
}
}
}
})requiredlists fields that must be present.bsonTypesets the type:'string','int','double','decimal','bool','date','objectId','array','object', or several, such as['double', 'int'].minimum,maximum,minLength,maxLength,patternandenumrestrict values.- Fields not mentioned are allowed, unless you set
additionalProperties: false, which then requires_idto be listed too.
Nested objects and arrays
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:
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:
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:
db.getCollectionInfos({ name: 'users' })[0].options.validatorSome 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.validatorMongoDB 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.
Rejecting invalid documents
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(){ ok: 1 }
saved
rejected (121)
rejected (121)
rejected (121)
saved
2Line-by-line explanation
- Lines 1 to 11The collection is created with rules for name, price and status.
- Line 7price may be a double or an integer, but not negative.
- Line 8status must be one of the listed values.
- Line 22A string price breaks the type rule; error 121 means document validation failed.
- Line 23A missing required field is rejected.
- Line 24A value outside the enum is rejected.
- Line 25Extra fields like colour are allowed, because additionalProperties isn't set to false.
Nested rules for orders
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(', '){ ok: 1 }
ok, invalid, invalid, invalidLine-by-line explanation
- Lines 6 to 8items must be an array with at least one element.
- Lines 9 to 12Every element must be an object with a sku and a positive integer qty.
- Line 20NumberInt makes the int type explicit, which matters in drivers and scripts where a plain number might be sent as a double.
- Line 21An empty order is rejected by minItems.
- Lines 22 to 23A zero quantity and a missing sku break the rules for array elements.
Adding rules to an existing collection
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}`) }{ acknowledged: true, insertedIds: { '0': 1, '1': 2, '2': 3 } }
[ { _id: 2 }, { _id: 3 } ]
1
1
insert rejected: 121Line-by-line explanation
- Line 8$jsonSchema in a query finds existing documents that would break the new rules.
- Line 9collMod adds the validator to the existing collection.
- Line 9The moderate level lets old invalid documents keep being updated while you fix them.
- Line 11New documents must follow the rules.
Common MongoDB Data Modelling Mistakes
Relying only on application checks
// validation lives only in the web app; an import script writes prices as stringsdb.runCommand({ collMod: 'products', validator: { $jsonSchema: { properties: { price: { bsonType: ['double', 'int', 'decimal'] } } } } })additionalProperties false without _id
{ $jsonSchema: { additionalProperties: false, properties: { name: { bsonType: 'string' } } } } // every insert fails: _id isn't listed{ $jsonSchema: { additionalProperties: false, properties: { _id: {}, name: { bsonType: 'string' } } } }Switching on strict rules over dirty data
db.runCommand({ collMod: 'users', validator: v }) // edits to old bad documents start failingdb.runCommand({ collMod: 'users', validator: v, validationAction: 'warn' }) // watch the logs, fix data, then switch to errorMongoDB 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.
Complete the schema so email must always be present.
{ $jsonSchema: { : ['email'] } }
What happens when an insert breaks a validator with the default settings?
Which keyword restricts a field to a list of allowed values?
What does validationLevel: 'moderate' do?
How can you find existing documents that break a schema?
MongoDB Data Modelling Quiz
5 questions to check what you learned.
Which keyword sets a field's type in $jsonSchema?
- bsonType
- type
- fieldType
- kind
How do you add validation to a collection that already exists?
- Drop and recreate it
- With the collMod command
- With createIndex
- It's impossible
What does validationAction: 'warn' do?
- Rejects invalid writes
- Deletes invalid documents
- Allows invalid writes but logs a warning
- Emails the admin
Are fields not listed in properties allowed?
- No, never
- Yes, unless additionalProperties is false
- Only strings
- Only in nested documents
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