skip to content

How would you add a $jsonSchema validator to a collection that already holds non-conforming documents?

level: seniorimportance: should knowfreq 44%

answer

  1. the command itself is metadata-only
  2. enforcement starts the instant you attach it
  3. start permissive, end strict
  4. you can query for the offenders directly
  5. negate a document expression with $nor

basics

~20 s

Roll it out in stages: attach the schema with collMod at validationLevel moderate and validationAction warn, measure the offenders by querying with $nor plus $jsonSchema, backfill them in batches while fixing the writers, then flip to strict and error.

solid answer

~50 s

Attaching a validator is a metadata change — `collMod` does not rescan or rewrite the collection — so the risk is not the operation, it is that enforcement starts instantly for live traffic. Stage it. First attach the schema with `validationLevel: "moderate"` and `validationAction: "warn"`: inserts and updates keep working, and violations are recorded in the mongod log. Second, size the problem with the `$jsonSchema` **query** operator — `db.orders.find({ $nor: [ { $jsonSchema: schema } ] })` returns exactly the documents that fail, and an aggregation over that set tells you which rule each one breaks. Third, fix the writers producing bad shapes, then backfill in bounded batches with `updateMany` or `bulkWrite` per defect class. Fourth, when the offending count is zero and the log is quiet, `collMod` to `"strict"` and `"error"`. Keep the rollback simple: another `collMod` puts the previous options back.

code

javascript · 7 lines
javascript
// attach in observation mode: nothing breaks, violations are logged
db.runCommand({
  collMod: "orders",
  validator: { $jsonSchema: candidateSchema },
  validationLevel: "moderate",
  validationAction: "warn"
})

go deeper

for a junior

Know that collMod is how a validator reaches an existing collection, and that documents already stored are not checked or changed when it is attached.

for a middle

Explain the permissive-to-strict ladder — moderate and warn first, strict and error last — and be able to write the $nor plus $jsonSchema query that finds the failing documents.

for a senior

Demonstrate the full rollout: measure, attribute failures per rule, fix writers before data, backfill in bounded batches, tighten in two moves, and keep a one-command rollback ready.

for a principal

Own the coordination: the ordering against rolling application deploys, who is accountable for the warning stream during the observation window, and how bypassDocumentValidation in restore tooling is governed afterwards.

## The operation is cheap; the exposure is not ```js db.runCommand({ collMod: "orders", validator: { $jsonSchema: schema } }) ``` That command edits the collection's options and returns. It does not scan the collection, does not rewrite documents, and does not need a maintenance window even on a very large collection. What it does do is switch enforcement on for every write already in flight. So the whole problem is sequencing: getting from "nothing is enforced" to "everything is enforced" without a period where legitimate application writes start failing. ## Step 1 — write the schema, attach it in observation mode Attach the candidate schema with the two options set to their permissive values: ```js db.runCommand({ collMod: "orders", validator: { $jsonSchema: candidateSchema }, validationLevel: "moderate", validationAction: "warn" }) ``` `warn` means a failing write is applied and logged rather than refused, so no application breaks. `moderate` additionally exempts updates to documents that already fail. If your goal in this phase is maximum signal rather than maximum safety, `strict` + `warn` is the better pairing — `moderate` suppresses the check on exactly the legacy documents you are trying to count. Either way, confirm the mongod log is shipped somewhere you can query before you rely on this phase for evidence. ## Step 2 — measure the blast radius `$jsonSchema` is not only a validator expression; it is a query operator. That gives you a direct way to enumerate the failing population: ```js db.orders.countDocuments({ $nor: [ { $jsonSchema: candidateSchema } ] }) ``` `$nor` with a single clause is the idiomatic negation here — `$not` is a field-level operator and cannot wrap a whole document expression at the top level. Run the same predicate against progressively smaller schemas (one rule at a time) to attribute the failures: how many documents are missing `customerId`, how many have `total` as a string, how many carry a `status` outside the enum. Those buckets are your backfill work items, and their sizes are what tells you whether this is an afternoon or a quarter. On a large collection this query is a collection scan, so run it on a secondary read or during a quiet period, and consider restricting it by a date range to sample first. ## Step 3 — fix the source before the data Backfilling before the writers are fixed just means doing it twice. The `warn` log and the per-rule counts together identify which services produce which defect. Ship those fixes first; only then does the offending count start monotonically decreasing. ## Step 4 — backfill in bounded batches One `updateMany` over millions of documents is a long-running write that competes with production traffic. Prefer a loop over batches keyed by `_id` ranges, or per-defect-class updates with a filter narrow enough to use an index: ```js db.orders.updateMany( { total: { $type: "string" } }, [ { $set: { total: { $toDecimal: "$total" } } } ] ) ``` This is where `moderate` earns its place: an update touching a document that still violates the schema in some other way is not blocked, so a multi-pass fixer can converge one defect at a time. And documents ratchet — once a document conforms, `moderate` will not let a later update degrade it. ## Step 5 — tighten, in two moves When the offending count is zero and the warning stream has gone quiet, tighten. Doing it in two steps is easier to reason about than one: 1. `validationAction: "error"` while still `moderate` — new bad data can no longer get in, and any surviving legacy document is still updatable. 2. `validationLevel: "strict"` — full enforcement. After each move, read the options back with `db.getCollectionInfos({ name: "orders" })`, because `collMod` sets the validation options you send it and you should never assume the ones you omitted. ## Things that will surprise you - **`bypassDocumentValidation`.** Privileged writes can skip validation entirely, and restore and import tooling commonly does. A restore of an old backup can reintroduce documents your validator would have refused. Know which of your operational paths use it. - **Rolling deploys.** If the new schema requires a field that only the new application version writes, the old version's writes fail the moment you flip to `error`. Deploy the writer first, enforce second. - **`additionalProperties: false`.** It is the most common cause of a validator that breaks the *next* deploy rather than this one, because adding a field becomes a schema change. Only use it where you genuinely want that coupling. - **Rollback.** There is always one: `collMod` back to the previous validator or to `validationLevel: "off"`. It is instantaneous. Write it down before you start so nobody has to improvise it during an incident.

  • How do you count the documents in a collection that a candidate schema would reject?
    Use $jsonSchema as a query operator and negate it: `db.orders.countDocuments({ $nor: [ { $jsonSchema: schema } ] })`. $nor is the right negation because $not is a field-level operator and cannot wrap a whole-document expression. Run per-rule variants of the schema to attribute failures to specific defects, and expect a collection scan.
  • Does collMod block writes or rewrite documents while it attaches the validator?
    No. It changes the collection's options and returns; existing documents are neither scanned nor modified. The risk is not duration but immediacy — enforcement applies to in-flight writes from that moment, which is why the first attach should carry permissive validationLevel and validationAction settings.
  • What can silently reintroduce documents that violate the validator after you have enforced it?
    Writes that set bypassDocumentValidation, which requires a specific privilege and is used by restore and bulk-import tooling. A restore from an old backup, or a migration job running with an elevated role, can land documents the validator would have refused. Audit which operational paths carry that privilege.
  • Why deploy the application change before tightening the validator, rather than together?
    Because during a rolling deploy both versions write concurrently. If the schema requires a field only the new version produces, every write from a not-yet-replaced old instance fails the moment the action becomes error. Ship the writer, confirm the offending count is zero, then tighten — the ordering makes the enforcement step a no-op.

saying these in an interview costs you the question

  • Assumes collMod rescans or rewrites the whole collection
  • Flips straight to strict plus error on a live collection
  • Backfills the data before fixing the services writing bad shapes
  • Negates a schema with $not at the top level instead of $nor
  • Forgets that restore and import tooling can bypass validation

context