MongoDB Cheatsheet
Schema Validation
Use this MongoDB reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Adding a validator
A validator is a query expression the server checks on every insert and update. In
practice that expression is $jsonSchema, optionally combined with $expr.
db.createCollection("orders", { validator: { $jsonSchema: { bsonType: "object", required: ["ref"] } }, validationLevel: "strict", validationAction: "error" }) // Add or change validation on an existing collection db.runCommand({ collMod: "orders", validator: { $jsonSchema: { bsonType: "object", required: ["ref", "status"] } }, validationLevel: "moderate", validationAction: "warn" }) // What is the current validator? db.getCollectionInfos({ name: "orders" })[0].options.validator
collMod replaces the validator wholesale, it does not merge, so read the current one
first if you are amending. Adding a validator never inspects the documents already in
the collection.
A realistic $jsonSchema
Nest properties to describe subdocuments and use items for arrays. Everything is
optional unless it is listed in required at that level.
db.runCommand({ collMod: "orders", validator: { $jsonSchema: { bsonType: "object", title: "order", required: ["ref", "customer_id", "status", "total", "items", "placed_at"], additionalProperties: false, properties: { _id: { bsonType: "objectId" }, ref: { bsonType: "string", pattern: "^ORD-[0-9]{8}$" }, customer_id: { bsonType: "objectId" }, status: { enum: ["pending", "paid", "shipped", "cancelled"] }, total: { bsonType: "decimal", minimum: 0 }, currency: { bsonType: "string", minLength: 3, maxLength: 3 }, placed_at: { bsonType: "date" }, tags: { bsonType: "array", uniqueItems: true, maxItems: 10, items: { bsonType: "string", minLength: 1 } }, items: { bsonType: "array", minItems: 1, items: { bsonType: "object", required: ["sku", "qty", "price"], additionalProperties: false, properties: { sku: { bsonType: "string", pattern: "^[A-Z]-[0-9]+$" }, qty: { bsonType: "int", minimum: 1, maximum: 999 }, price: { bsonType: "decimal", minimum: 0 } } } }, address: { bsonType: "object", required: ["city", "country"], properties: { city: { bsonType: "string" }, country: { bsonType: "string", pattern: "^[A-Z]{2}$" }, zip: { bsonType: ["string", "null"] } } } } } } })
Keywords that work
| Keyword | Applies to | Notes |
|---|---|---|
bsonType | Any | BSON aliases: objectId date decimal long int double binData bool array object null. Accepts an array of allowed types |
type | Any | The plain JSON Schema keyword. Knows nothing about objectId or date, so prefer bsonType |
required | object | Array of property names required at that level |
properties | object | Per-field subschemas |
additionalProperties: false | object | Reject any field not named in properties. Remember _id |
patternProperties | object | Subschemas keyed by a regex on the field name |
minProperties · maxProperties | object | Field count |
enum | Any | Allowed values. Works for strings, numbers, anything |
minimum · maximum | number | Inclusive. exclusiveMinimum: true flips one bound |
multipleOf | number | Divisibility |
minLength · maxLength · pattern | string | pattern is a regex string, no // delimiters |
minItems · maxItems · uniqueItems | array | Length and distinctness |
items | array | One subschema for every element, or an array of subschemas positionally |
oneOf · anyOf · allOf · not | Any | Combinators, for tagged unions |
description · title | Any | Free text, surfaced in the validation error |
bsonType: "int" is strict. A shell literal 3 is a double, so it fails an int
schema and you have to write NumberInt(3). Either use "number" in the schema or be
deliberate about numeric types on the write path.
validationLevel and validationAction
Two orthogonal dials: which writes get checked, and what happens when a check fails.
validationLevel | Checks |
|---|---|
strict (default) | Every insert and every update |
moderate | All inserts, but updates only on documents that already satisfy the schema. Existing invalid documents can still be updated |
off | Nothing. The validator is retained but dormant |
validationAction | On failure |
|---|---|
error (default) | Reject the write, raise DocumentValidationFailure (code 121) |
warn | Allow the write, log a warning in the mongod log |
The migration path for an existing collection is warn then error:
// 1. Observe without breaking anything db.runCommand({ collMod: "orders", validator: schema, validationAction: "warn", validationLevel: "moderate" }) // 2. Find and fix the offenders db.orders.find({ $nor: [ { $jsonSchema: schema } ] }).count() // 3. Enforce db.runCommand({ collMod: "orders", validator: schema, validationAction: "error", validationLevel: "strict" })
$jsonSchema is a query operator too, which is what makes step 2 possible. Wrapping it
in $nor finds every document the schema would reject.
Reading a validation error
Since 5.0 the error carries a structured errInfo naming the rule that failed, which
beats the older "Document failed validation" with no detail.
db.orders.insertOne({ ref: "BAD", status: "paid", total: NumberDecimal("1") })
MongoServerError: Document failed validation
Additional information: {
failingDocumentId: ObjectId('6612...'),
details: {
operatorName: '$jsonSchema',
schemaRulesNotSatisfied: [
{ operatorName: 'required',
specifiedAs: { required: [ 'ref', 'customer_id', ... ] },
missingProperties: [ 'customer_id', 'items', 'placed_at' ] },
{ operatorName: 'properties',
propertiesNotSatisfied: [
{ propertyName: 'ref', description: 'order',
details: [ { operatorName: 'pattern',
specifiedAs: { pattern: '^ORD-[0-9]{8}$' },
reason: 'regular expression did not match',
consideredValue: 'BAD' } ] } ] }
]
}
}Walk details.schemaRulesNotSatisfied. missingProperties covers required,
propertiesNotSatisfied covers everything else, and each leaf gives you
specifiedAs, reason, and consideredValue. Put real text in description on the
tricky fields, because it shows up here.
A privileged client can skip validation for one call:
db.orders.insertOne(legacyDoc, { bypassDocumentValidation: true })
$expr for cross-field rules
$jsonSchema cannot compare two fields to each other. $expr can, and the two combine
with $and.
db.runCommand({ collMod: "orders", validator: { $and: [ { $jsonSchema: { bsonType: "object", required: ["total", "discount"] } }, { $expr: { $lte: ["$discount", "$total"] } }, { $expr: { $eq: ["$item_count", { $size: "$items" }] } }, { $expr: { $or: [ { $eq: ["$status", "shipped"] }, { $eq: [{ $type: "$shipped_at" }, "missing"] } ] } } ] } })
Any aggregation expression works inside $expr, including $size, $type,
$dateDiff, and $switch. It runs per document on every write, so keep it cheap.
A $expr validator cannot reference another document, only fields of the one being
written.
Where validation stops
This is a guardrail, not a relational schema. Be honest about the gap.
| Not available | Use instead |
|---|---|
| Uniqueness | A unique index. Validators never see other documents |
| Foreign keys and cascades | Application code, or embed the data so there is nothing to reference |
| Default values | Set them on write, or a $setOnInsert on upsert |
| Type coercion | Nothing coerces. A string "5" is rejected, not converted |
| Cross-document invariants | A transaction, or a document design that makes the invariant local |
| Retroactive checks | collMod never scans existing documents. Sweep with $nor + $jsonSchema yourself |
| Friendly messages | errInfo is for engineers. Map it, or validate before the write |
The practical split: a schema validator is the last line of defence, so it catches the bug that shipped and the script somebody ran by hand. Application-level validation (Mongoose schemas, a Zod parse at the API boundary) is the first line, and it is where defaults, coercion, and the messages a user reads belong. Run both. Neither replaces the other, and a validator that duplicates your Zod schema is doing exactly its job when it fires.