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

KeywordApplies toNotes
bsonTypeAnyBSON aliases: objectId date decimal long int double binData bool array object null. Accepts an array of allowed types
typeAnyThe plain JSON Schema keyword. Knows nothing about objectId or date, so prefer bsonType
requiredobjectArray of property names required at that level
propertiesobjectPer-field subschemas
additionalProperties: falseobjectReject any field not named in properties. Remember _id
patternPropertiesobjectSubschemas keyed by a regex on the field name
minProperties · maxPropertiesobjectField count
enumAnyAllowed values. Works for strings, numbers, anything
minimum · maximumnumberInclusive. exclusiveMinimum: true flips one bound
multipleOfnumberDivisibility
minLength · maxLength · patternstringpattern is a regex string, no // delimiters
minItems · maxItems · uniqueItemsarrayLength and distinctness
itemsarrayOne subschema for every element, or an array of subschemas positionally
oneOf · anyOf · allOf · notAnyCombinators, for tagged unions
description · titleAnyFree 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.

validationLevelChecks
strict (default)Every insert and every update
moderateAll inserts, but updates only on documents that already satisfy the schema. Existing invalid documents can still be updated
offNothing. The validator is retained but dormant
validationActionOn failure
error (default)Reject the write, raise DocumentValidationFailure (code 121)
warnAllow 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 availableUse instead
UniquenessA unique index. Validators never see other documents
Foreign keys and cascadesApplication code, or embed the data so there is nothing to reference
Default valuesSet them on write, or a $setOnInsert on upsert
Type coercionNothing coerces. A string "5" is rejected, not converted
Cross-document invariantsA transaction, or a document design that makes the invariant local
Retroactive checkscollMod never scans existing documents. Sweep with $nor + $jsonSchema yourself
Friendly messageserrInfo 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.