Validation Levels and Actions
When you attach a $jsonSchema validator to a MongoDB collection, two settings decide how seriously MongoDB takes it: validationLevel controls which writes get checked, and validationAction controls what happens when a document fails. Together they let you roll out schema enforcement gradually instead of flipping a switch that instantly breaks every write from an older app version or a collection full of legacy documents.
This lesson assumes you already know how to define a validator with $jsonSchema (covered in the previous lesson). Here we focus entirely on the two settings that decide how that validator actually behaves in production.
Overview: How Validation Enforcement Works
MongoDB documents are schema-flexible by default — two documents in the same collection can have completely different fields. A validator does not change that storage model; it is a set of rules that insert and update operations are checked against before the write is applied. The validator itself (usually a $jsonSchema object) defines what a valid document looks like. validationLevel and validationAction decide how strictly and how loudly that definition is enforced.
validationLevel answers: which write operations does the validator apply to?
- strict (the default) — every insert and every update is checked, full stop. If an existing document in the collection already violates the schema, any update to that document is also checked and will be rejected unless the update brings the document into compliance.
- moderate — inserts are always checked, but updates are only checked on documents that already satisfy the validator. Documents that predate the validator (and don’t conform to it) can continue to be updated freely without being forced to fix their whole shape in one write. This is the escape hatch for adding validation to a collection that already has messy legacy data.
- off — the validator is stored on the collection but not enforced at all. Useful for temporarily disabling validation without deleting the schema definition, e.g. during a bulk migration.
validationAction answers: what happens when a write fails validation?
- error (the default) — the write is rejected outright. The driver or mongosh raises an error and nothing is written.
- warn — the write is allowed to proceed even though it violates the schema, but MongoDB logs a warning message to the server log (
mongodlog, not visible in the shell response). This is a monitoring tool, not an enforcement tool — invalid data still lands in your collection.
These two knobs are independent and combine into a small matrix: strict+error is the strictest (default) mode, moderate+warn is the loosest mode that still gives you visibility, and so on. A common real-world pattern is to launch a new validator as moderate+warn, watch the logs for a week to see how much existing traffic would have failed, then tighten to strict+error once the warnings die down.
Syntax
You set these options either when creating the collection, or later against an existing collection with collMod.
db.createCollection("collectionName", {
validator: { /* $jsonSchema or query-style validation rules */ },
validationLevel: "strict", // "off" | "strict" | "moderate"
validationAction: "error" // "error" | "warn"
});
// Or change the settings on a collection that already exists:
db.runCommand({
collMod: "collectionName",
validationLevel: "moderate", // optional, omit to leave unchanged
validationAction: "warn" // optional, omit to leave unchanged
});
| Setting | Value | Meaning |
|---|---|---|
validationLevel |
strict |
Default. Validates all inserts and all updates, including updates to already-invalid documents. |
validationLevel |
moderate |
Validates all inserts, but only validates updates to documents that already pass the schema. |
validationLevel |
off |
Validator is stored but not enforced on any write. |
validationAction |
error |
Default. Rejects the write and returns an error to the client. |
validationAction |
warn |
Allows the write and logs a warning to the mongod log instead of rejecting it. |
Examples
Example 1: strict + error (the default, strongest enforcement)
db.createCollection("orders", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["customerId", "status", "total"],
properties: {
customerId: {
bsonType: "objectId",
description: "must be an ObjectId and is required"
},
status: {
enum: ["pending", "shipped", "delivered", "cancelled"],
description: "must be one of the allowed values and is required"
},
total: {
bsonType: "double",
minimum: 0,
description: "must be a non-negative number and is required"
}
}
}
},
validationLevel: "strict",
validationAction: "error"
});
Output:
{ ok: 1 }
A valid insert sails through:
db.orders.insertOne({
customerId: new ObjectId(),
status: "pending",
total: 49.99
});
Output:
{
acknowledged: true,
insertedId: ObjectId("66b8f2a1c3d4e5f6a7b8c9d0")
}
But an insert with an out-of-enum status is rejected before it ever reaches storage:
db.orders.insertOne({
customerId: new ObjectId(),
status: "processing",
total: 19.99
});
Output:
MongoServerError: Document failed validation
Additional information: {
failingDocumentId: ObjectId("66b8f2b7c3d4e5f6a7b8c9d1"),
details: {
operatorName: '$jsonSchema',
schemaRulesNotSatisfied: [
{
operatorName: 'properties',
propertiesNotSatisfied: [
{
propertyName: 'status',
description: 'must be one of the allowed values and is required',
details: [
{
operatorName: 'enum',
specifiedAs: { enum: [ 'pending', 'shipped', 'delivered', 'cancelled' ] },
reason: 'value was not found in enum',
consideredValue: 'processing'
}
]
}
]
}
]
}
}
Nothing was written — the client gets an exception it can catch and handle. This is the safest configuration and the right default for most production collections once their schema is settled.
Example 2: moderate — adding validation to a collection with legacy data
Say orders already has thousands of old documents inserted before you ever wrote a validator, and some of them are missing total entirely. Switching straight to strict would mean any innocuous update to those old documents (even one unrelated to total) starts failing. moderate avoids that:
db.runCommand({
collMod: "orders",
validationLevel: "moderate"
});
Output:
{ ok: 1 }
With moderate in place, new inserts are still fully checked against the schema. But an update to a pre-existing order that was already missing total is left alone — MongoDB only re-validates documents that were valid going in. This buys you time to backfill the missing fields with a background migration script instead of an emergency one.
Example 3: warn — observing violations without blocking writes
During that migration window, you might want visibility into how much of your write traffic would actually fail strict validation, without blocking any of it yet:
db.runCommand({
collMod: "orders",
validationAction: "warn"
});
db.orders.insertOne({
customerId: new ObjectId(),
status: "processing", // not in the enum
total: -5 // violates minimum: 0
});
Output:
{
acknowledged: true,
insertedId: ObjectId("66b8f31ac3d4e5f6a7b8c9d2")
}
The document was written despite violating two schema rules — warn never rejects a write. Instead, mongod appends a log line to the server log noting the collection, the failing document, and the schema rule that was not satisfied. You can confirm the current settings on a collection at any time:
db.getCollectionInfos({ name: "orders" });
Output:
[
{
name: 'orders',
type: 'collection',
options: {
validator: { $jsonSchema: { /* ... */ } },
validationLevel: 'moderate',
validationAction: 'warn'
},
info: { readOnly: false }
// ...
}
]
How It Works Step by Step
For every insert or update MongoDB executes on a validated collection, the storage layer runs this sequence before the write is committed:
- 1. Determine if the write is in scope. Under
strict, every write is in scope. Undermoderate, an update is only in scope if the document’s pre-update state already satisfied the validator; an insert is always in scope. - 2. Evaluate the validator expression. The resulting document (post-insert or post-update) is matched against the
$jsonSchema(or query-style validator). This is a pure evaluation — no data is persisted yet. - 3. Branch on the result. If it passes, the write proceeds normally regardless of
validationAction. If it fails: undererrorthe operation aborts and the storage engine discards the pending write, returning an error to the caller; underwarnthe storage engine commits the write anyway and appends a diagnostic entry to the server log. - 4. Replication. Once a write is committed on the primary (whether it violated the schema under
warnor was clean), it replicates to secondaries as-is — validators run only on the primary at write time, not on replication or on reads.
Note the last point carefully: find() queries never re-check the validator. If bad data gets into the collection via warn, direct writes from a script that bypasses your app layer, or bypassDocumentValidation: true, it stays there until something explicitly fixes or removes it.
Common Mistakes
Mistake 1: Flipping straight to strict on a collection with dirty legacy data
Wrong:
// orders already has documents missing `total`
db.runCommand({
collMod: "orders",
validationLevel: "strict",
validationAction: "error"
});
// Now this innocent status update on an old order fails:
db.orders.updateOne(
{ _id: ObjectId("5f43a1b2c3d4e5f6a7b8c9d3") },
{ $set: { status: "shipped" } }
);
// MongoServerError: Document failed validation (total is missing)
The update had nothing to do with total, but strict re-validates the whole resulting document, so the pre-existing gap blocks an unrelated field change. Fix it by staying on moderate until a migration backfills the missing fields, then tighten to strict:
db.runCommand({ collMod: "orders", validationLevel: "moderate" });
// ... run a migration to backfill `total` on old documents ...
db.runCommand({ collMod: "orders", validationLevel: "strict" });
Mistake 2: Treating warn as if it were enforcement
Wrong assumption: leaving validationAction: "warn" in production because “the schema is documented and being checked.” It is checked, but never enforced — invalid documents keep accumulating silently, and nobody notices until a report or an aggregation crashes on an unexpected type.
// This silently succeeds and pollutes the collection under `warn`:
db.orders.insertOne({ customerId: "not-an-objectid", status: "pending", total: 10 });
warn is a temporary observability tool for migrations, not a production safety net. Once you’ve confirmed (via the logs) that new writes are clean, switch to error:
db.runCommand({ collMod: "orders", validationAction: "error" });
Best Practices
- Keep
strict+erroras your steady-state configuration; only relax it temporarily during a schema rollout or migration. - When adding a validator to a collection that already has data, run
find()with the inverse of your schema rules first (or usedb.runCommand({ collMod: ..., validator: {...} })with a `dryRun`-style check in a staging environment) to estimate how much existing data would fail. - Use
moderate+warnas the rollout phase, watch themongodlog for validation warnings, then graduate tostrict+error. - Remember that
bypassDocumentValidation: trueon an individual write (or certain privileged operations) skips the validator entirely — restrict who can use it, since it’s an easy way to reintroduce bad data even understrict+error. - Re-check settings with
db.getCollectionInfos({ name: "yourCollection" })before assuming a collection is strictly enforced — it’s easy to forget a collection was left inwarnmode after a migration. - Validators don’t run on reads or on replication, so periodically audit older data with a script if you ever ran under
warnoroff, rather than assuming the schema is universally true.
Practice Exercises
- Create a collection
productswith a$jsonSchemavalidator requiringname(string) andprice(non-negative double), usingvalidationLevel: "strict"andvalidationAction: "error". Confirm a document with a negative price is rejected. - Insert two documents into a fresh, unvalidated collection where one is missing a required field. Then add the same validator from the exercise above with
validationLevel: "moderate". Verify that updating the invalid document still succeeds while a brand-new invalid insert is rejected. - Switch that same collection’s
validationActiontowarn, insert an invalid document, and confirm withdb.getCollectionInfos()that the settings showwarnwhile the insert still returnsacknowledged: true.
Summary
validationLevelcontrols which writes are checked:strict(all),moderate(inserts plus updates to already-valid documents), oroff(none).validationActioncontrols the consequence of a failed check:errorrejects the write,warnlogs it but still allows it.- The production default is
strict+error; usemoderate/warnas temporary tools during rollout or migration, not as permanent settings. - Change either setting on an existing collection with
db.runCommand({ collMod: "name", validationLevel: ..., validationAction: ... }). - Validators run only at write time on the primary — not on reads, not on replication — so data written under
warnoroffcan remain invalid indefinitely unless explicitly fixed.
