MongoDB Mastery
MongoDB Mastery lessons / Module 21: Schema Validation

Schema validation with $jsonSchema

Level 3: Advanced20 min

Enforce required fields, types, ranges and enumerations on the server, and roll validation out safely with validationLevel and validationAction.

This lesson is interactive. Run every example against an in-browser MongoDB, see explain() plans drawn as trees, and take the quiz.
Open the interactive lesson
You will learn to
  • Write a $jsonSchema validator
  • Add validation to an existing collection with collMod
  • Choose validationLevel (strict, moderate) and validationAction (error, warn)
  • Read validation error details

conceptThe concept

db.createCollection("payments", { validator: { $jsonSchema: {
  bsonType: "object",
  required: ["orderId", "amount", "currency", "status"],
  properties: {
    amount:   { bsonType: ["double", "decimal", "int"], minimum: 0 },
    currency: { enum: ["INR", "USD", "EUR"] },
    status:   { enum: ["created", "captured", "refunded"] },
    card:     { bsonType: "object", properties: { last4: { bsonType: "string", pattern: "^[0-9]{4}$" } } }
  },
  additionalProperties: true
} }, validationLevel: "strict", validationAction: "error" })
  • validationLevel: "strict" (default) validates all inserts and updates; "moderate" skips updates to documents that were already invalid, which helps when introducing rules on existing data.
  • validationAction: "error" rejects with code 121 (DocumentValidationFailure) and explains which rule failed; "warn" accepts the write and logs a warning.
  • Validators can also use query operators: { $or: [{ phone: { $type: "string" } }, { email: { $regex: /@/ } }] }.
  • MongoDB 9.0+ validationLevel: "constraint" goes further than strict: MongoDB guarantees every document in the collection satisfies the rules, so readers can trust old data too. The simulator in this course is 8.0 and does not accept it.

whyWhy it matters

Flexible schemas still need guarantees: a payment without an amount is a bug, not flexibility. Server-side validation catches errors from every client, script and service, not just the one with good application-level checks.

Use caseReal-world scenario

A team adds validation to a 5-year-old collection. Step 1: validationAction: "warn" and watch logs for a week. Step 2: fix the producers and clean the data. Step 3: switch to "error" with validationLevel: "moderate", and later "strict".

Try itExamples

Run and edit these in the interactive lesson.

Create a validated collection
db.lab_payments.drop()
db.createCollection("lab_payments", { validator: { $jsonSchema: {
  bsonType: "object",
  required: ["orderNo", "amount", "currency", "status"],
  properties: {
    amount: { bsonType: ["double", "int", "decimal"], minimum: 0 },
    currency: { enum: ["INR", "USD", "EUR"] },
    status: { enum: ["created", "captured", "refunded"] }
  } } } })
db.lab_payments.insertOne({ orderNo: "ORD-1", amount: 99.5, currency: "INR", status: "created" })
A rejected write and its explanation
db.lab_payments.insertOne({ orderNo: "ORD-2", amount: -5, currency: "GBP" })
Roll out on existing data with warn and moderate
db.runCommand({ collMod: "customers", validator: { $jsonSchema: { required: ["email", "tier"], properties: { tier: { enum: ["bronze", "silver", "gold", "platinum"] } } } }, validationLevel: "moderate", validationAction: "warn" })
db.customers.deleteOne({ _id: 9001 })
db.customers.insertOne({ _id: 9001, name: "No email" })
db.getCollectionInfos({ name: "customers" })[0].options

internalsUnder the hood

The validator is stored in collection options and evaluated on the primary during each write. It does not apply to existing documents until they are updated. bypassDocumentValidation lets privileged users skip validation for migrations. Validation is cheap compared with the write itself, but very large schemas with many patterns add CPU per write.

Avoid / preferDo this, not that

Avoid
db.runCommand({ collMod: "customers", validator: { $jsonSchema: schema },
  validationAction: "error" })   // on day one, on old data
Prefer
db.runCommand({ collMod: "customers", validator: { $jsonSchema: schema },
  validationAction: "warn" })
db.customers.countDocuments({ $nor: [{ $jsonSchema: schema }] })   // fix these, then "error"

Switching straight to error breaks every update to an existing invalid document. Warn first, find the offenders with $nor, fix them, then enforce.

Avoid
{ $jsonSchema: { properties: { email: { bsonType: "string" } } } }
Prefer
{ $jsonSchema: { required: ["email"],
  properties: { email: { bsonType: "string", pattern: "^[^@]+@[^@]+$" } } } }

properties only checks fields that are present, so a document without email passes. List mandatory fields in required.

PitfallsCommon mistakes

  • Turning on strict/error validation on a collection with existing invalid documents and breaking every update to them.
  • Over-specifying (additionalProperties: false everywhere) and blocking legitimate schema evolution.
  • Forgetting that numbers from JavaScript are doubles: bsonType: "int" rejects 5 sent as a double by some drivers.
  • Relying only on validation for business rules that span documents (it validates one document at a time).

LimitsWhen not to use it

Validators check one document at a time. Uniqueness needs a unique index, and references to other collections need application logic. Very complex regex patterns cost CPU on every write. Users with the bypassDocumentValidation privilege can skip validation entirely.

SecuritySecurity notes

Do not grant bypassDocumentValidation to application roles. On collections that hold privileges or money, add additionalProperties: false so unexpected fields such as isAdmin are rejected instead of stored.

ChallengeTry it yourself

Create a scratch collection members_lab that requires email (a string containing @) and tier (bronze, silver, gold or platinum). Insert one valid document and show that an invalid one is rejected.

Show one solution
One solution
db.members_lab.drop()
db.createCollection("members_lab", { validator: { $jsonSchema: {
  bsonType: "object", required: ["email", "tier"],
  properties: { email: { bsonType: "string", pattern: "@" }, tier: { enum: ["bronze", "silver", "gold", "platinum"] } }
} } })
db.members_lab.insertOne({ email: "a@x.com", tier: "gold" })
try { db.members_lab.insertOne({ email: "nope", tier: "diamond" }) } catch (e) { print("rejected:", e.code) }
db.members_lab.countDocuments()

InterviewQuestions you should be able to answer

How do you introduce schema validation on a large existing collection?

Write the validator, run it as a query to count non-conforming documents (find({ $nor: [validator] })), set validationAction: "warn" to observe, fix producers and backfill data in batches, then switch to error with validationLevel: "moderate", and finally strict once all documents conform.

QuizCheck your understanding

4 questions with instant feedback in the interactive lesson; a wrong answer goes into your review deck.