Repair AtlasMart historical defaults and derived fields without conflating null with missing or fabricating values when trustworthy source data is absent.

Defaults, Missing Fields, Derived Data, and Repairing Historical Documents

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.

Intermediate90–120 minutesNull/missing + deterministic repair labMongoDB 8.3.8 · mongosh 2.10.0Last reviewed: September 2026

Learning outcomes

AtlasMart's new order model expects a shipping method and caches totalCents for fast reads. Historical documents, however, contain three different states: the field is missing, the field is explicitly null, or the derived total is stale. Treating all three as “bad data” and overwriting them with one default can erase meaning. A repair migration must distinguish absence from explicit unknown state and must recompute derived values only from trustworthy source fields.

01

Explain why MongoDB schema validation does not materialize default values and why the JSON Schema default keyword is not a supported MongoDB validator keyword.

02

Distinguish missing fields from explicit null in queries and migration filters.

03

Choose between read-time defaults, write-time defaults, and historical backfills based on semantic ownership and query/index consequences.

04

Identify derived fields whose source components are sufficient for deterministic repair and quarantine cases where source truth is missing.

05

Build an idempotent repair that retains the prior derived value for a controlled rollback.

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:27050, mongosh 2.10.0, and PyMongo 4.17.0 where driver behavior is shown. The database is atlasmart; the collection is orders_ch07_l4. 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. A validator constrains values; it does not execute defaults

MongoDB's $jsonSchema implementation supports a documented subset of JSON Schema and explicitly omits the default keyword. Even in JSON Schema ecosystems where default is treated as annotation, validation itself is not a database defaulting engine. If AtlasMart needs every new order to store shippingMethod:"standard", the writer should set it, an update/migration should materialize it, or a server-side update expression should do so. Do not rely on a validator to create data.

Default strategy Benefit Cost / semantic risk
Read-time application default No historical write storm; easy rollback. Stored queries/analytics still see missing; different applications can choose inconsistent defaults.
Write-time default in every writer New data becomes explicit and queryable. All writers/jobs must implement the same contract; old writers remain a compatibility risk.
Historical backfill One stored shape simplifies indexes/analytics. Creates real write/replication load and can overwrite semantic distinctions if filters are too broad.
Derived field cache Reduces repeated computation and can support indexes/sorts. Creates synchronization/reconciliation responsibility; source-of-truth rules must be explicit.

2. Missing is not null, and {field:null} is intentionally broad

MongoDB equality to null matches documents where the field is explicitly null or does not exist. That is useful for some queries but dangerous in a repair script. In AtlasMart, an explicit null shipping method means “business process marked the method unknown,” while a missing field means “legacy writer predated the field.” Those are different histories and should not be collapsed accidentally.

javascript · the broad null repair that erases meaning, followed by the precise missing-only filter
// WRONG: equality to null matches explicit null and missing.db.orders_ch07_l4.updateMany(  { shippingMethod:null },  { $set:{shippingMethod:"standard"} });// This destroys the business distinction "explicitly unknown" vs "not written yet".// Safer when the business rule is “default only absent historical values”:db.orders_ch07_l4.updateMany(  { shippingMethod:{ $exists:false } },  { $set:{shippingMethod:"standard"} });

If the business wants explicit null to mean the same thing as missing, document that invariant first and then merge them deliberately. The database syntax should follow semantics, not create them.

3. Repair only what can be derived from trustworthy stored components

bash · isolated Chapter 07 Lesson 4 lab setup
docker rm -f atlasmart-mongo-ch07-l4 2>/dev/null || truedocker volume rm atlasmart-mongo-ch07-l4-data 2>/dev/null || truedocker run -d --name atlasmart-mongo-ch07-l4 \  -p 127.0.0.1:27050:27017 \  -v atlasmart-mongo-ch07-l4-data:/data/db \  mongodb/mongodb-community-server:8.3.8-ubuntu2204-slimmongosh "mongodb://127.0.0.1:27050/atlasmart?directConnection=true" --quiet --eval \'printjson({server:db.version(), hello:db.hello().isWritablePrimary}); printjson(db.getSiblingDB("admin").runCommand({getParameter:1,featureCompatibilityVersion:1}))' 
javascript · inspect null/missing and repair deterministic derived totals
const orders = db.getCollection("orders_ch07_l4");orders.drop();orders.insertMany([  { _id:"o-7401", schemaVersion:NumberInt(2), subtotalCents:NumberInt(10000), shippingCents:NumberInt(500), taxCents:NumberInt(800), discountCents:NumberInt(0), totalCents:NumberInt(11300), shippingMethod:"standard" },  { _id:"o-7402", schemaVersion:NumberInt(2), subtotalCents:NumberInt(5000), shippingCents:NumberInt(500), taxCents:NumberInt(400), discountCents:NumberInt(300), totalCents:NumberInt(999999) },  { _id:"o-7403", schemaVersion:NumberInt(2), subtotalCents:NumberInt(2000), shippingCents:NumberInt(0), taxCents:NumberInt(160), discountCents:NumberInt(0), totalCents:NumberInt(2160), shippingMethod:null },  { _id:"o-7404", schemaVersion:NumberInt(2), subtotalCents:NumberInt(3000), shippingCents:NumberInt(500), discountCents:NumberInt(0), totalCents:NumberInt(3500) }]);print("{shippingMethod:null} matches null AND missing:");printjson(orders.find({shippingMethod:null},{_id:1,shippingMethod:1}).sort({_id:1}).toArray());print("missing only:", orders.countDocuments({shippingMethod:{$exists:false}}));print("explicit null only:", orders.countDocuments({ $and:[ {shippingMethod:null}, {shippingMethod:{$exists:true}} ] }));// Safe default repair: only missing, not explicit null.printjson(orders.updateMany(  { shippingMethod:{$exists:false} },  { $set:{ shippingMethod:"standard" } }));const expectedTotal = {  $subtract:[    { $add:["$subtotalCents","$shippingCents","$taxCents"] },    "$discountCents"  ]};print("repairable total mismatches:");printjson(orders.find({  subtotalCents:{$type:"int"}, shippingCents:{$type:"int"}, taxCents:{$type:"int"}, discountCents:{$type:"int"},  $expr:{ $ne:["$totalCents", expectedTotal] }},{_id:1,totalCents:1}).toArray());print("unresolved: missing source fields:");printjson(orders.find({ $or:[{taxCents:{$exists:false}},{subtotalCents:{$exists:false}},{shippingCents:{$exists:false}},{discountCents:{$exists:false}}] },{_id:1}).toArray());const repair = orders.updateMany(  {    "migration.ch07L4.applied":{$ne:true},    subtotalCents:{$type:"int"}, shippingCents:{$type:"int"}, taxCents:{$type:"int"}, discountCents:{$type:"int"},    $expr:{ $ne:["$totalCents", expectedTotal] }  },  [    { $set:{      "migration.ch07L4.previousTotalCents":"$totalCents",      "migration.ch07L4.applied":true,      totalCents:expectedTotal    } }  ]);printjson(repair);print("remaining repairable mismatch:", orders.countDocuments({  subtotalCents:{$type:"int"}, shippingCents:{$type:"int"}, taxCents:{$type:"int"}, discountCents:{$type:"int"},  $expr:{ $ne:["$totalCents", expectedTotal] }}));

Order o-7402 has all four components needed to recompute the total, so the repair can be deterministic. Order o-7404 lacks taxCents; inventing tax from today's policy would rewrite history incorrectly. It remains in an unresolved set for business-specific reconciliation. This is the difference between data cleanup and data fabrication.

The migration marker records the original totalCents only on the first repair and prevents a rerun from overwriting that rollback value. In production, a migration marker may be temporary or kept in an audit trail; either way, define ownership and cleanup before adding arbitrary metadata to every document.

4. Derived data needs a freshness contract

A cached total, review count, inventory summary, or normalized search field is derived data: it can be recomputed from an authoritative source. Duplication can be valuable for read locality, but a stale derived field is a correctness defect if consumers mistake it for the source of truth. Specify when it is updated, how retries are idempotent, how drift is detected, and which query/report can reconcile it.

For high-value financial history, “recompute” may itself be unsafe if formulas, currencies, tax policy, or rounding rules changed. Store the inputs/version needed to reproduce the original calculation or treat the historical amount as authoritative. Schema repair should never silently apply current business policy to past events.

Data-quality query as a permanent control

Keep the mismatch predicate—or an equivalent scheduled metric/test—after the migration. A successful one-time backfill proves only that the sampled state was clean at that moment. It does not prove future writers cannot reintroduce drift.

5. Verification, cleanup, and production judgment

Verification checklist

  • The broad {shippingMethod:null} query returns both explicit-null and missing documents.
  • The safe default backfill changes only documents where the field is missing.
  • The derived-data query detects o-7402 as a repairable mismatch.
  • The script separately reports records that lack source components instead of fabricating a value.
  • The repair stores the previous total and marks the document so a retry is a no-op.
  • A post-repair mismatch query reaches zero for the repairable subset while unresolved records remain visible.

Defaults and derived fields are modeling choices with read/write/index/storage consequences. Materializing a default makes it indexable and explicit but costs writes and storage. Leaving it missing avoids migration work but forces every consumer to preserve the same interpretation. Derived copies reduce read computation but increase update amplification and reconciliation burden. None of these choices improves durability or consistency automatically; those guarantees depend on the write path, concerns, topology, and invariant scope.

Backfills can be hot-aggregate or shard-skew amplifiers when many affected documents share a partition pattern. Observe latency tails, replication lag, disk/cache pressure, write tickets, and index growth. Use least-privilege maintenance identities and tenant-scoped filters. Keep rollback data or restore capability for destructive repairs.

The next lesson combines the chapter into a deployment gate: production-like samples, validator tests, migration idempotency, failure injection, cutover, and rollback rehearsal before enforcement reaches live data.

bash · cleanup / full reset
docker rm -f atlasmart-mongo-ch07-l4docker volume rm atlasmart-mongo-ch07-l4-data

Check your understanding

  1. Why does adding a validator with a conceptual default not populate missing fields?
  2. Why is {{field:null}} dangerous for a migration that should touch only missing fields?
  3. When is a derived field safe to recompute automatically?
  4. Why does the repair store the old total before replacing it?
  5. Why should the mismatch query survive after the one-time migration?
Review the answers

MongoDB schema validation constrains writes; it does not materialize defaults, and the MongoDB $jsonSchema implementation does not support the default keyword.

Equality to null matches both explicit null and field absence. A missing-only migration needs $exists:false or an equally precise predicate.

When all authoritative source inputs and the correct historical calculation rules are available and the transformation is deterministic. Otherwise the record should be reviewed/quarantined rather than guessed.

It creates a controlled rollback path for the synthetic repair and prevents the migration from destroying the only evidence of the prior value.

Because future writers can reintroduce drift. The predicate becomes a regression/observability control rather than a one-time migration tool.

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.