Setting up validationAction warn vs error in production

Choosing between validationAction: "warn" and validationAction: "error" is the single decision that determines whether a MongoDB collection silently accumulates schema drift or rejects malformed writes at the persistence layer, and getting it wrong in production costs you either data integrity or availability. This page is a precise operational playbook — part of Implementing Collection-Level Validators within the broader Automated Schema Enforcement & Monitoring framework — that shows exactly how each mode behaves on the write path, how to read the failure fingerprints each one emits, and how to transition from warn to error with zero downtime and a sub-minute rollback. The reader deliverable is a repeatable collMod sequence you can run against a live collection and revert in seconds if write failures spike.

Operational Mechanics and Write-Path Impact

MongoDB evaluates a $jsonSchema validator synchronously during every insert, update, and replace. The validationAction parameter decides what the server does the instant a document violates that schema. With error (the default), the storage engine rejects the write and returns WriteError code 121 (Document failed validation) — the only setting that guarantees strict compliance at rest. With warn, the server accepts and persists the document exactly as submitted, acknowledges the write as successful to the client, and records the violation only as a structured entry (log id 51803) in the mongod diagnostic log. There is no type coercion, no required-field backfill, and no enum enforcement in warn mode — the raw document lands in the collection unchanged.

validationAction never acts alone; it is gated by validationLevel, which decides which documents are checked. strict validates all inserts and updates; moderate skips validation for updates to documents that already violate the schema, so only new inserts and updates to currently-valid documents are evaluated. The combined behavior is the matrix you actually deploy against:

validationAction validationLevel Checks applied to On violation Production use
warn moderate Inserts + updates to already-valid docs Logs id 51803; write succeeds Observe drift on a legacy collection without blocking anything
warn strict All inserts and updates Logs id 51803; write succeeds Force legacy docs to surface violations on their next update
error moderate Inserts + updates to already-valid docs Rejects with WriteError 121 Enforce the contract while tolerating pre-existing bad rows
error strict All inserts and updates Rejects with WriteError 121 Full enforcement — the end state for a hardened collection

The dangerous quadrant is warn + moderate: legacy documents stay unchecked, new invalid documents are silently accepted, and downstream aggregation pipelines begin failing on unexpected nulls or type mismatches with no signal at the write path. Validation itself adds negligible per-document CPU under normal load, but schemas with deep $and/$or nesting or unanchored $regex patterns can amplify write latency under burst ingestion, so treat a complex validator as an active component of write-path cost rather than free metadata.

Exact Diagnostic Fingerprints and Fast Resolution

The two modes leave completely different traces, and knowing which surface to inspect is what makes triage fast. In error mode the signal is on the client: the driver raises WriteError with code == 121 and a nested errInfo.details.schemaRulesNotSatisfied array that names the exact failing operator and JSON path. In warn mode the client sees a normal WriteResult and the only record is server-side — a structured JSON log entry keyed by id: 51803. You cannot diagnose warn violations from the application; you must read the diagnostic log.

To extract failing operators and missing properties from an exported or tailed mongod log:

grep '"id":51803' mongod.log | python3 -c "
import sys, json
for line in sys.stdin:
    try:
        entry = json.loads(line)
        for err in entry.get('attr', {}).get('validationErrors', []):
            print(err.get('operatorName'), err.get('missingProperties'))
    except Exception:
        pass
"

On the driver side, catch the exception and route on the code so a misconfigured client is diverted rather than crashing the ingestion loop:

from pymongo.errors import WriteError

try:
    collection.insert_one(document)
except WriteError as exc:
    if exc.code == 121:
        details = exc.details.get("errInfo", {}).get("details", {})
        rules = details.get("schemaRulesNotSatisfied", [])
        dead_letter.publish(document, reason=rules)  # do not drop the payload
    else:
        raise

Feeding those schemaRulesNotSatisfied paths into fallback validation chains or a dead-letter queue is what turns a rejection into a recoverable event instead of silent data loss, and categorizing schema validation errors by operator name lets you tell a transient application bug apart from a genuine migration gap. For authoritative operator semantics, consult the official MongoDB schema validation documentation.

Step-by-Step Playbook

Migrating from warn to error on a live collection is a phased, observable rollout. Five steps take you from blind enforcement risk to full strict + error with an audit trail at every stage.

  1. Deploy in warn + moderate. Attach the validator without blocking any write, and let it run across at least one peak-traffic window (24–48 hours) to capture real drift:
    db.runCommand({
      collMod: "orders",
      validator: { $jsonSchema: { /* your schema */ } },
      validationLevel: "moderate",
      validationAction: "warn"
    })
    Expected output: { ok: 1 } with no write errors on the client.
  2. Quantify the violation rate. Watch Atlas alerts for validation failures on logId:51803, or parse the diagnostic log with the grep snippet above. If the rate is non-trivial, stop and fix the ingestion pipeline before proceeding — do not promote a collection you cannot keep clean.
  3. Backfill and normalize. Align existing documents with the schema using an unordered bulk write so failures isolate instead of aborting the batch:
    collection.bulk_write(operations, ordered=False)
  4. Switch to strict + warn. Keep writes flowing but force every legacy document to be validated on its next update, surfacing any remaining violations to log id 51803 without rejecting anything:
    db.runCommand({ collMod: "orders", validationLevel: "strict", validationAction: "warn" })
  5. Cut over to error. Once 51803 events hold at zero for 48 hours, promote to full enforcement:
    db.runCommand({
      collMod: "orders",
      validationLevel: "strict",
      validationAction: "error"
    })
    Every collMod here is metadata-only and takes effect immediately on the primary, propagating to secondaries through the oplog.
Phased warn-to-error promotion with a violation gate and 60-second rollback A collection validator is deployed in warn plus moderate to observe drift over a 24 to 48 hour window. A decision gate checks whether the log id 51803 violation rate holds under 0.1 percent. If not, ingestion is remediated and the collection stays in warn. Once the gate passes, the collection moves to strict plus warn to force legacy compliance, then cuts over to strict plus error for full enforcement. A dashed amber arrow shows the soft rollback from the error cutover back to warn, taken within 60 seconds if write failures spike. Deploy $jsonSchema warn + moderate observe drift · 24–48h violation rate under 0.1%? log id 51803 strict + warn force legacy compliance Cutover to error strict · full enforcement Fix ingestion remediate · backfill yes no write failures spike → soft rollback within 60s

Failure Modes & Rollback

The failure that hurts is a write-path cascade immediately after the step-5 cutover: a client you did not account for starts emitting non-compliant writes, every one throws WriteError 121, and its retry loop hammers the primary. The rollback is a single metadata-only command that stops rejections without discarding the schema, and it recovers in the time it takes to propagate through the oplog — typically seconds:

// Soft rollback: keep the schema, stop rejecting (time-to-recover: seconds).
db.runCommand({ collMod: "orders", validationAction: "warn" })

Treat this soft rollback as non-negotiable and pre-stage the command before every cutover. If the collection needs to be fully unlocked — for example a broken schema was deployed — remove the validator entirely:

// Hard rollback: drop enforcement completely.
db.runCommand({ collMod: "orders", validator: {}, validationLevel: "off" })

Two other failure modes are worth pre-empting. First, never disable validation globally to relieve one misbehaving client — isolate that client with a feature flag or route it to a shadow collection while the rest of the workload keeps its guarantees. Second, after a high-volume rejection cycle, run db.orders.validate() to confirm index consistency and BSON storage integrity before declaring the incident closed. Rejected payloads should be replayed from the dead-letter queue by a background worker once the schema mismatch is patched at the application layer, not re-sent blindly into the same failing path.

Frequently Asked Questions

Does switching validationAction with collMod block in-flight writes?

The change is metadata-only and takes an exclusive collection lock for a brief moment on the primary. In-flight writes already accepted are unaffected; new writes are evaluated against whichever action was in force when they arrived. Because it propagates through the oplog, secondaries adopt the new action after their replication lag, so a soft rollback's true time-to-recover is the lag of your slowest secondary.

If I run warn, will the invalid documents ever be rejected later?

No. A document persisted under warn is stored exactly as submitted and is never retroactively rejected. It will only be re-checked if it is updated while validationLevel is strict. This is why the safe promotion path passes through strict + warn before error — it forces legacy rows to surface their violations to log id 51803 without failing the write.

Can I detect warn-mode violations from the application code?

Not directly. In warn mode the driver returns a normal WriteResult and no exception is raised, so the only signal is the server-side id: 51803 log entry. To alert on drift you must ingest those log events — through Atlas log-based alerts or a shipped mongod log — rather than inspecting driver responses.