Gate MongoDB schema enforcement with production-like synthetic samples, validator preflight, migration idempotency, failure cases, old/new client tests, and rollback rehearsal.

Test Validators and Migration Scripts Against Production-Like Samples Before Enforcement

Batch heterogeneous writes safely, interpret partial success, compare ordered and unordered execution, and use modern cross-namespace bulk APIs without assuming all-or-nothing behavior.

Intermediate110–145 minutesMigration acceptance harness + enforcement gateMongoDB 8.3.8 · mongosh 2.10.0 · PyMongo 4.17.0Last reviewed: September 2026

Learning outcomes

AtlasMart is ready to enforce version 2 of the customer schema, but a migration that works on three hand-picked documents is not evidence that it is safe for millions. A production-like sample preserves the important shape/cardinality/error strata of production without copying secrets or personal data. A migration acceptance harness runs candidate validation, repair, idempotency, compatibility, and rollback tests before enforcement reaches the live collection.

01

Build a deterministic synthetic sample containing common, optional, legacy, already-migrated, and deliberately poisoned document strata.

02

Use the candidate $jsonSchema as a preflight and postflight quality query.

03

Gate enforcement on an idempotent migration rerun, supported-version distribution, and old/new writer behavior.

04

Measure batch timing as environment-specific evidence without turning lab values into universal tuning advice.

05

Treat bypassDocumentValidation, untested rollback, and “zero exceptions in a tiny happy-path sample” as explicit failure modes.

Reproducible lab baseline

This lesson pins MongoDB Community Server 8.3.8 with image mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim, a disposable standalone mongod bound only to 127.0.0.1:27051, mongosh 2.10.0, and PyMongo 4.17.0 where driver behavior is shown. The database is atlasmart; the collection is customers_ch07_l5. Authentication and TLS are disabled only because this is an isolated loopback learning container. The lab reads FCV but never changes it. Default read/write concern and primary read preference are used on this standalone. Atlas, Search, KMS, Enterprise Advanced, and paid services are not required. Product commands below were reviewed against current official documentation; they were not executed in this generation environment because Docker/mongod/mongosh/PyMongo are unavailable here.

How to read expected output

Result fragments labeled “expected” are contract-oriented shapes derived from the documented command semantics, not copied from a generation-time MongoDB run. Field order, error wording, log attributes, object identifiers, timing, and some diagnostic detail are version/environment dependent. Verify the invariant or count being taught rather than comparing output byte-for-byte.

1. Production-like does not mean production data copied to a laptop

The sample must preserve behavioral dimensions: schema versions, optional-field absence, explicit null, long/empty strings, unusual but valid values, repairable legacy aliases, malformed types, and enough documents to exercise batches. It should not contain real customer identifiers, credentials, tokens, payment data, or confidential text. Generate deterministic synthetic values or sanitize data under a governed process.

Sample stratum Why include it Acceptance question
Common v1 records Dominant migration path. Does the transform produce v2 and remain idempotent?
Optional field missing/null Query/default edge cases. Do readers/validators preserve the intended distinction?
Already-v2 records Retry/restart reality. Does the migration leave target-state documents alone?
Legacy alias/shape Long-tail historical drift. Can the transform repair from trustworthy source fields?
Poisoned target-version records Silent corruption / bad earlier deployment. Does the candidate validator detect target-state documents that only claim to be v2?
Boundary values Lengths, ranges, nested arrays/objects. Does enforcement reject only semantically invalid boundaries?

2. Seed a deterministic 500-document AtlasMart sample

bash · isolated Chapter 07 Lesson 5 lab setup
docker rm -f atlasmart-mongo-ch07-l5 2>/dev/null || truedocker volume rm atlasmart-mongo-ch07-l5-data 2>/dev/null || truedocker run -d --name atlasmart-mongo-ch07-l5 \  -p 127.0.0.1:27051:27017 \  -v atlasmart-mongo-ch07-l5-data:/data/db \  mongodb/mongodb-community-server:8.3.8-ubuntu2204-slimmongosh "mongodb://127.0.0.1:27051/atlasmart?directConnection=true" --quiet --eval \'printjson({server:db.version(), hello:db.hello().isWritablePrimary}); printjson(db.getSiblingDB("admin").runCommand({getParameter:1,featureCompatibilityVersion:1}))' 
python · synthetic production-like fixture generator
from pymongo import MongoClientURI = "mongodb://127.0.0.1:27051/atlasmart?directConnection=true"client = MongoClient(URI, serverSelectionTimeoutMS=3000)coll = client.atlasmart.customers_ch07_l5coll.drop()docs = []# 350 ordinary v1 documents.for i in range(350):    docs.append({"_id": f"c-{i:04d}", "name": f"Customer {i}", "email": f"c{i}@example.test", "phone": None if i % 11 == 0 else f"+994-{100000+i}"})# 80 v1 documents with no optional phone field.for i in range(350, 430):    docs.append({"_id": f"c-{i:04d}", "name": f"Customer {i}", "email": f"c{i}@example.test"})# 50 already-v2 documents.for i in range(430, 480):    docs.append({"_id": f"c-{i:04d}", "schemaVersion": 2, "name": f"Customer {i}", "contact": {"email": f"c{i}@example.test", "phone": None}})# 15 repairable legacy variants: email stored under legacyEmail.for i in range(480, 495):    docs.append({"_id": f"c-{i:04d}", "name": f"Customer {i}", "legacyEmail": f"c{i}@example.test"})# 5 deliberately poisoned v2 samples: contact.email has wrong type but a trusted old email remains.for i in range(495, 500):    docs.append({"_id": f"c-{i:04d}", "schemaVersion": 2, "name": f"Customer {i}", "email": f"c{i}@example.test", "contact": {"email": i, "phone": None}})coll.insert_many(docs, ordered=True)print("seeded", coll.count_documents({}))client.close()

The fixture deliberately contains five documents whose schemaVersion already says 2 while contact.email has the wrong BSON type. A migration that filters only “version < 2” would miss them. This catches a common governance mistake: trusting a version marker more than the actual candidate schema.

3. Run the acceptance harness before strict enforcement

python · preflight, staged validator, migration, repair, idempotency, and cutover gates
import statistics, timefrom pymongo import MongoClient, UpdateOnefrom pymongo.errors import WriteErrorURI = "mongodb://127.0.0.1:27051/atlasmart?directConnection=true"client = MongoClient(URI, serverSelectionTimeoutMS=3000)db = client.atlasmartcoll = db.customers_ch07_l5schema_v2 = {    "bsonType": "object",    "required": ["_id", "schemaVersion", "name", "contact"],    "properties": {        "_id": {"bsonType": "string"},        "schemaVersion": {"bsonType": "int", "enum": [2]},        "name": {"bsonType": "string", "minLength": 1},        "contact": {            "bsonType": "object",            "required": ["email"],            "properties": {                "email": {"bsonType": "string", "pattern": "@example\\.test$"},                "phone": {"bsonType": ["string", "null"]},            },        },    },}invalid = {"$nor": [{"$jsonSchema": schema_v2}]}def invalid_count(): return coll.count_documents(invalid)def distribution():    return list(coll.aggregate([        {"$group": {"_id": {"$ifNull": ["$schemaVersion", 1]}, "count": {"$sum": 1}}},        {"$sort": {"_id": 1}},    ]))print("initial distribution", distribution())print("candidate invalid", invalid_count())# Observation mode first; inserts that violate are allowed but logged.db.command({"collMod": "customers_ch07_l5", "validator": {"$jsonSchema": schema_v2}, "validationLevel": "moderate", "validationAction": "warn"})batch_times_ms = []BATCH = 50  # lab visibility only; not a production tuning value.while True:    source = list(coll.find({"$or": [{"schemaVersion": {"$exists": False}}, {"schemaVersion": 1}]}).sort("_id", 1).limit(BATCH))    if not source: break    ops = []    for d in source:        email = d.get("email") or d.get("legacyEmail")        if not email:            continue        ops.append(UpdateOne(            {"_id": d["_id"], "$or": [{"schemaVersion": {"$exists": False}}, {"schemaVersion": 1}]},            {"$set": {"schemaVersion": 2, "contact": {"email": email, "phone": d.get("phone")}}},        ))    t0 = time.perf_counter()    if ops: coll.bulk_write(ops, ordered=True)    batch_times_ms.append((time.perf_counter() - t0) * 1000)# Repair the deliberately poisoned synthetic v2 records from their retained trusted source.repair = coll.update_many(    {"schemaVersion": 2, "contact.email": {"$not": {"$type": "string"}}, "email": {"$type": "string"}},    [{"$set": {"contact.email": "$email"}}],)print("poison repair modified", repair.modified_count)print("after migration distribution", distribution())print("candidate invalid after repair", invalid_count())# Idempotency gate: source-version migration must now do zero work.rerun = coll.update_many(    {"$or": [{"schemaVersion": {"$exists": False}}, {"schemaVersion": 1}]},    [{"$set": {"schemaVersion": 2}}],)print("idempotency rerun modified", rerun.modified_count)if batch_times_ms:    ordered = sorted(batch_times_ms)    p95 = ordered[max(0, int(0.95 * len(ordered)) - 1)]    print("lab batch ms median/p95", round(statistics.median(ordered), 2), round(p95, 2))assert invalid_count() == 0assert rerun.modified_count == 0# Cutover only after the gates pass.db.command({"collMod": "customers_ch07_l5", "validator": {"$jsonSchema": schema_v2}, "validationLevel": "strict", "validationAction": "error"})try:    coll.insert_one({"_id": "old-client", "name": "Old", "email": "old@example.test"})    raise AssertionError("old-client write unexpectedly passed")except WriteError:    print("old-client write rejected as expected")coll.insert_one({"_id": "new-client", "schemaVersion": 2, "name": "New", "contact": {"email": "new@example.test", "phone": None}})print("new-client accepted", coll.count_documents({"_id": "new-client"}))client.close()

The exact median/p95 batch times depend on CPU, storage, cache warmth, Python/runtime, container virtualization, and host load; they are not supplied as expected constants. What the harness can assert deterministically is the state contract: after repair the candidate-invalid count is zero, the source-version rerun modifies zero documents, the old writer is rejected after cutover, and the new writer is accepted.

Measure failure as well as success

Before production enforcement, add a controlled failure test: interrupt the migration between batches, restart from its checkpoint/query state, and prove convergence; simulate a bad old writer; and rehearse restoring the pre-migration sample. On a replica set, also test backfill pacing against replication lag and election/retry behavior. Do not inject failures into unrelated or production data.

4. Deliberately wrong: bypass validation to make the migration green

MongoDB exposes bypassDocumentValidation for privileged maintenance workflows. That is a capability boundary, not a correctness shortcut. If a migration bypasses validation and the acceptance test only checks that writes were acknowledged, corrupted documents can enter the collection and strict enforcement can later trap them there. With access control enabled, use of the option requires the corresponding privilege action; application roles should not receive it casually.

python · anti-pattern and safer acceptance gates
# Anti-pattern: make the migration "look successful" by bypassing validation# and then enable strict enforcement without checking stored data.collection.insert_many(import_rows, bypass_document_validation=True)# A successful write result proves only the bypassed command ran; it does not prove data quality.# Safer gate:# 1) candidate-invalid count by $jsonSchema# 2) migration rerun modifies 0# 3) distribution matches supported versions# 4) old/new writer compatibility tests pass# 5) rollback/restore rehearsal succeeds# 6) only then switch to strict/error

A useful test suite also asserts that the ordinary application identity cannot bypass the rule. This chapter does not build production authentication—that is covered later—but the deployment checklist must record who can execute collMod and who can bypass validation.

5. Enforcement gate, rollback rehearsal, and production judgment

Verification checklist

  • The synthetic dataset has 500 documents and contains all declared schema strata.
  • The preflight candidate-invalid count is nonzero, proving the test actually contains migration work and poisoned target-state data.
  • The migration repairs source-version documents and separately repairs the five deliberately poisoned v2 documents from retained trustworthy values.
  • The final candidate-invalid count is zero.
  • An immediate rerun of the source migration modifies zero documents.
  • Strict/error cutover rejects the old-client shape and accepts the new-client shape.
  • The reset path can recreate the exact original synthetic sample; production rollout has a separately tested backup/restore or reversible-data plan.

Validation rollout is a production change with latency, storage, index, replication, sharding, security, and organizational consequences. Test with realistic document sizes and index sets, not only counts. On replica sets, monitor oplog growth and lag; on sharded clusters, understand whether the migration predicate targets shards or broadcasts. If a backfill touches every document, it can compete with foreground traffic and backup windows. Use measured pacing and pause criteria.

Validators guarantee only that qualifying writes satisfy the declared predicate at write time. They do not guarantee semantic correctness, authorization, uniqueness unless separately indexed, cross-document invariants, backup recoverability, or future compatibility. A migration is complete when data, applications, operational runbooks, rollback, observability, and enforcement all agree—not when one script exits zero.

This chapter closes the flexible-schema foundation. Chapter 08 moves into the aggregation pipeline, where document streams pass through ordered stages and the same schema/type discipline determines whether transformations are correct and efficient.

bash · cleanup / full reset
docker rm -f atlasmart-mongo-ch07-l5docker volume rm atlasmart-mongo-ch07-l5-data

Check your understanding

  1. Why should a production-like migration sample include already-v2 but invalid documents?
  2. What does a second migration run with modified_count == 0 prove?
  3. Why are lab batch median/p95 values not valid production tuning defaults?
  4. What is dangerous about bypassDocumentValidation in a migration?
  5. What conditions should be true before strict/error enforcement is enabled?
Review the answers

Because a version marker can be wrong or an earlier writer can have produced a malformed target-shape document. The candidate schema must validate actual content, not trust the marker.

It is evidence that the tested transformation is idempotent for the current dataset/source predicate. It does not prove concurrency, every hidden edge case, or production performance.

Timing depends on hardware, virtualization, storage/cache state, indexes, document sizes, topology, concurrent load, and driver/runtime details. Production pacing must be measured there.

It can let invalid writes succeed and hide data-quality problems from a test that only checks acknowledgement. In production it is also a privilege that must be tightly controlled.

Candidate-invalid debt is zero or explicitly accepted, migration reruns are no-ops, supported-version distribution is understood, old/new writers/readers are tested, rollback/restore is rehearsed, monitoring/pause criteria exist, and the responsible identities/privileges are correct.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.