Snapshotting Validator State Before collMod
Capturing a collection’s exact validator state the instant before a collMod mutates it is the precondition that makes every schema change reversible, and it is the specific procedure this page delivers under rollback automation for schema changes within the wider Zero-Downtime Schema Migration Automation in MongoDB framework. The reader outcome is a runnable capture function that serializes the current validator, validationLevel, and validationAction into a durable, timestamped artifact keyed to a commit — a stored fact that a later restore replays byte-for-byte instead of reconstructing from memory during an incident. The trap this avoids is the most common one in schema automation: applying a change with no recorded pre-state, so that the only path back is a human recalling what the rule used to look like while the write path is stalled. A correct snapshot removes that guesswork entirely.
Operational Mechanics and Write-Path Impact
A validator is not stored inside documents; it lives in the collection’s catalog entry alongside its indexes and options. That placement is what makes snapshotting cheap and exact: reading it is a metadata lookup, not a scan. Two catalog-facing commands expose it. In mongosh, db.getCollectionInfos({ name: "<coll>" })[0].options returns the validator, validationLevel, and validationAction as they truly are on the primary. From a driver, the same data arrives through listCollections with a name filter, under cursor.firstBatch[0].options. Both read the authoritative state — the ground truth the storage engine enforces — not whatever a deploy script last intended.
The capture itself has zero write-path impact. It issues no collMod, takes no exclusive lock, and touches no document, so it can run against a hot production collection at any moment without a stall. This is deliberate: the snapshot must be safe to take unconditionally, immediately before every change, because a capture that carried operational risk would tempt engineers to skip it — the exact circumstance under which an irreversible change slips through.
What must survive serialization is the full three-field triple, because enforcement posture is defined by all three together. A validator document alone is ambiguous: the same $jsonSchema behaves completely differently under strict + error than under moderate + warn. A snapshot that records only the schema and discards the level and action can restore a collection to a runtime state it was never actually in.
The other serialization hazard is type fidelity. A $jsonSchema freely uses BSON constructs — bsonType: "date", bsonType: "objectId", bsonType: "decimal" — and the validator document can itself contain typed literals in enum arrays or minimum/maximum bounds. If you serialize that to plain JSON with a naive encoder, a Decimal128 bound or a date literal collapses to a string or a float, and the restored validator is subtly different from the original. The fix is to serialize with MongoDB Extended JSON (canonical mode), which round-trips every BSON type losslessly. PyMongo ships this in bson.json_util; storing the artifact as a BSON document in a MongoDB collection sidesteps the problem entirely because BSON is the native representation.
Exact Diagnostic Fingerprints and Fast Resolution
Snapshot failures are quiet — a missing or lossy capture causes no error until you try to restore it, which is the worst possible moment to discover the problem. These are the signatures to detect proactively.
| Signature | Where it appears | Root cause | Resolution |
|---|---|---|---|
Restore replays an empty validator: {} when the collection had a rule |
On restore, enforcement unexpectedly detaches | Capture ran against the wrong namespace or before the validator existed | Assert the artifact’s validator is non-empty when a rule was expected; re-snapshot from the correct collection |
A date or decimal bound comes back as a string or float |
On restore or artifact review | Serialized with a plain JSON encoder, not Extended JSON | Re-serialize the live triple with bson.json_util.dumps in canonical mode |
KeyError: 'validationLevel' when reading the artifact |
In restore code | Only the validator was captured; level/action dropped |
Capture all three fields; default missing ones to off/error explicitly |
Two artifacts for one collection with the same captured_at window |
In the snapshot store | A change was applied without its own fresh capture | Enforce one snapshot per change in the applying automation |
listCollections returns an empty firstBatch |
During capture | Collection does not exist yet on this node | Treat a non-existent collection as the sentinel triple: empty validator, level off |
Confirm a capture is faithful by round-tripping it: serialize, deserialize, and compare the result to the live options field-for-field before you trust it as a rollback target.
from bson import json_util
def is_faithful(live_options: dict, serialized: str) -> bool:
"""A snapshot is only usable if it deserializes back to the live triple exactly."""
restored = json_util.loads(serialized)
return all(
restored.get(k) == live_options.get(k)
for k in ("validator", "validationLevel", "validationAction")
)
Step-by-Step Playbook
The capture is a short, deterministic sequence. Each step is safe to re-run and never touches document data.
-
Read the live triple from the catalog. Use
listCollectionswith a name filter and takeoptionsfrom the first batch entry. This is authoritative state, not deploy-script intent:from pymongo import MongoClient client = MongoClient("mongodb://localhost:27017/?replicaSet=rs0") db = client.billing def read_triple(coll: str) -> dict: info = db.command("listCollections", filter={"name": coll}) batch = info["cursor"]["firstBatch"] opts = batch[0].get("options", {}) if batch else {} return { "validator": opts.get("validator", {}), "validationLevel": opts.get("validationLevel", "off"), "validationAction": opts.get("validationAction", "error"), } # read_triple("invoices") -> {'validator': {...}, 'validationLevel': 'strict', ...} -
Serialize with type fidelity. Encode the triple as canonical Extended JSON so every BSON type in the schema survives a round-trip:
from bson import json_util def serialize(triple: dict) -> str: return json_util.dumps(triple, json_options=json_util.CANONICAL_JSON_OPTIONS) # Dates and Decimal128 bounds come back as {"$date": ...} / {"$numberDecimal": ...} -
Store the artifact with a timestamp and commit. Persist to a durable location keyed to the collection, so a restore can find the most recent pre-change state:
from datetime import datetime, timezone def store_snapshot(coll: str, commit: str) -> dict: triple = read_triple(coll) artifact = { "coll": coll, "captured_at": datetime.now(timezone.utc), "commit": commit, **triple, # native BSON types persist losslessly in a MongoDB doc } artifact["_id"] = db._schema_snapshots.insert_one(artifact).inserted_id return artifact # store_snapshot("invoices", "a1b9f3c") -> artifact with a stored _id -
Verify before you proceed. Round-trip the artifact against the live options and refuse to continue if they diverge — an unfaithful snapshot is worse than none, because it looks safe:
live = read_triple("invoices") assert is_faithful(live, serialize(live)), "snapshot is not faithful; do not apply the change"
Only after step 4 passes should the collMod that mutates the validator run. The snapshot and the change belong to the same automation transaction of intent: capture, verify, then mutate.
Failure Modes & Rollback
Each step has a distinct way to fail, and because the snapshot itself writes nothing to the target collection, “rollback” here means discarding a bad artifact and re-capturing rather than reversing a data change.
- Capture against the wrong node (step 1). Reading from a lagging secondary can return an older triple than the primary currently enforces. Read with primary read preference during a migration so the snapshot matches the state the change is about to mutate. Recovery: re-read from the primary and discard the stale artifact.
- Silent type loss on serialize (step 2). A plain
json.dumpswill raise on adatetimeor coerce aDecimal128, producing an artifact that restores a subtly wrong schema. Recovery: always usebson.json_util; the round-trip assertion in step 4 catches any encoder that slips through. - Store write rejected (step 3). If the
_schema_snapshotscollection has its own validator, a malformed artifact insert can fail with code121. Keep the snapshot store schema-light or unvalidated. Recovery: the change must not proceed until the artifact is durably stored — treat a failed snapshot insert as a hard stop. - Verification mismatch (step 4). A non-zero diff means the capture is not a trustworthy rollback target. Recovery: do not apply the
collMod; re-run the capture and investigate the divergence before mutating anything.
The genuine rollback these snapshots enable is on the change they precede, not on the capture itself. Once a stored artifact exists, reverting the subsequent collMod is a single metadata-only command whose time-to-recover is the replication lag of the slowest secondary — typically seconds. Generating that revert automatically from the stored artifact is the whole of automating collMod rollback scripts.
Frequently Asked Questions
Why not just re-derive the old validator from version control instead of snapshotting?
Because version control records what you intended to deploy, not what is actually enforced on the collection right now. Drift is common: a manual collMod, a partially-applied migration, or a concurrent change can leave the live validator different from the last committed schema. Snapshotting reads the catalog directly, so the artifact is the ground truth the storage engine holds — the only state a restore can safely return to.
What breaks if I serialize the snapshot with a plain JSON encoder?
BSON types that JSON cannot represent are lost or mangled. A bsonType: "date" literal bound becomes a string, a Decimal128 minimum becomes a float, and an objectId collapses to a hex string. The restored validator then differs from the original in ways that only surface as unexpected acceptances or rejections later. Serialize with MongoDB Extended JSON in canonical mode, or store the artifact as a native BSON document, so every type round-trips losslessly.
Does taking a snapshot affect live traffic on the collection?
No. The capture is a metadata read through listCollections or getCollectionInfos. It issues no collMod, acquires no exclusive lock, and never touches a document, so it is safe to run against a hot collection immediately before every change. That zero-cost property is intentional — a capture with any operational risk would be skipped under pressure, which is precisely when an irreversible change slips through.
Related
- Rollback Automation for Schema Changes — the parent workflow this capture step feeds, covering the full snapshot-apply-restore subsystem.
- Zero-Downtime Schema Migration Automation in MongoDB — the section reference for coupling a schema change with the data migration it accompanies.
- Automating collMod Rollback Scripts — the sibling procedure that turns the artifact captured here into an executable one-click revert.
- Schema Versioning Strategies for NoSQL — treating each stored snapshot as the previous version of a versioned schema contract.