Chapter 02 · BSON, Documents, Collections, Flexible Schema, and Data Types
Flexible Schema Does Not Mean No Schema: Implicit Contracts and Application Evolution
Treat flexible schema as an explicit evolving application contract, with compatibility windows, version markers, validation, migration evidence, and rollback.
Learning outcomes
AtlasMart deploys a new catalog service that adds
schemaVersion, changes price representation from a
plain number to a structured decimal/currency object, and
introduces optional seller metadata. MongoDB allows old and new
documents to coexist—but the application, indexes, validators,
exports, analytics, and migrations still need a contract.
“Flexible schema” shifts where schema is managed; it does not
remove schema.
Identify implicit schemas created by application code, indexes, validators, encryption, analytics, and operational tooling.
Design backward/forward-compatible document evolution using explicit version markers and tolerant readers.
Distinguish missing fields, explicit null, defaults, and incompatible type changes during rolling deployments.
Use a lightweight MongoDB validator to expose bad writes without turning this chapter into the dedicated validation course chapter.
Plan migration, verification, rollback, and observability before changing a high-volume collection shape.
Mandatory server examples use a disposable loopback-only
standalone
mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim. Driver examples pin pymongo==4.17.0. The lab
is intentionally unauthenticated only because it is
short-lived and published to 127.0.0.1; Chapter
01 already demonstrated the authenticated reusable lab. Do not
expose this container on an untrusted interface. Atlas is
optional and not required.
The build environment does not provide Docker, mongod, or mongosh, and cannot install PyMongo from the network. Product/driver commands were reviewed against current official documentation but were not executed here. Expected-output blocks describe stable evidence shape, not fabricated captured output. Learners should record their actual versions and outputs.
1. A schema exists wherever code assumes a shape
If AtlasMart code executes doc["price"]["amount"],
it assumes price exists, is an object, and
amount has a compatible numeric type. That is a
schema even if the database has no validator. Compound indexes
assume fields/types/cardinality; encryption rules assume paths;
BI pipelines assume columns; change-stream consumers assume
event/document shape; backup/restore tests assume compatibility
with deployed binaries and code.
Flexible documents are valuable because different product categories and migration states can coexist. They are risky when teams confuse “the server accepts it” with “every reader understands it.” The correct goal is controlled heterogeneity: know which variants are allowed, for how long, and how each writer/reader behaves.
| Contract source | Example assumption | Failure if ignored |
|---|---|---|
| Application model | price.amount is Decimal128 |
Type error or silent numeric coercion |
| Index | sku exists and is selective |
Unexpected query behavior/cost |
| Validator | schemaVersion and required fields |
Write rejected/warned depending action |
| Analytics/export | one price representation | Nulls, parsing failures, incorrect totals |
| Event consumer | field renamed atomically everywhere | Older consumer breaks during rolling deploy |
2. Evolve with explicit versions and compatibility windows
Suppose version 1 stored price: 18.5. Version 2
stores
price:{amount:Decimal128("18.50"),currency:"USD"}.
A rolling deployment means old and new code can run
simultaneously. A safe migration can use an explicit
schemaVersion, a reader that understands both
versions, a writer that emits only the new version after all
readers are compatible, a backfill with idempotent filters,
verification, and finally removal of legacy handling.
Do not write two authoritative fields indefinitely (“price” and “newPrice”) without ownership rules. Dual representation creates divergence risk. Define which field is source-of-truth during the migration and when the old representation becomes forbidden.
// v1 legacy product{ _id:"sku-mug-blue", schemaVersion:1, price:18.5 }// v2 product{ _id:"sku-mug-blue", schemaVersion:2, price:{ amount:Decimal128("18.50"), currency:"USD" }}// Reader concept (pseudocode)function readMoney(doc) { if (doc.schemaVersion === 2) return doc.price; if (doc.schemaVersion === 1) return {amount: decimalFromLegacy(doc.price), currency:"USD"}; throw new Error("unsupported schemaVersion");}
3. Null, missing, and defaults must be migration decisions
Adding seller.displayName does not automatically
populate old documents. A missing field may mean “created before
the feature,” while explicit null may mean “known to have no
value,” “redacted,” or “not yet supplied.” If the application
collapses both into an empty string, it loses useful state. A
default applied only in application code can also create a split
between persisted state and displayed state.
Write down the semantics. If a field becomes required, decide whether to backfill old documents first, use a compatibility period, or let the reader compute a default. Do not introduce a strict validator before existing data and rolling writers satisfy it.
4. Use validation as one contract layer—not the only one
Chapter 07 teaches validation deeply. Here we use a minimal
$jsonSchema validator only to make schema evolution
observable. MongoDB's JSON Schema support is based on draft 4
with MongoDB-specific differences and
bsonType awareness. The validator can reject or
warn on writes depending on configuration, but it cannot express
every application invariant or guarantee every external consumer
is compatible.
docker rm -f atlasmart-mongo-ch02-l4 2>/dev/null || truedocker run --name atlasmart-mongo-ch02-l4 -p 127.0.0.1:27025:27017 -d mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim
mongosh "mongodb://127.0.0.1:27025/atlasmart?directConnection=true" --quiet --eval 'db.flex_products.drop();db.createCollection("flex_products", {validator:{$jsonSchema:{ bsonType:"object", required:["schemaVersion","sku","price"], properties:{ schemaVersion:{enum:[1,2]}, sku:{bsonType:"string"}, price:{oneOf:[ {bsonType:["double","int","long","decimal"]}, {bsonType:"object", required:["amount","currency"], properties:{ amount:{bsonType:"decimal"}, currency:{bsonType:"string"} }} ]} }}}});db.flex_products.insertMany([ {schemaVersion:1,sku:"legacy-1",price:18.5}, {schemaVersion:2,sku:"modern-1",price:{amount:Decimal128("18.50"),currency:"USD"}}]);try { db.flex_products.insertOne({schemaVersion:2,sku:"bad-1",price:"18.50"}); }catch(e){ print(e.codeName || e.code, e.message); }printjson(db.runCommand({listCollections:1,filter:{name:"flex_products"}}).cursor.firstBatch[0].options);'
The two allowed representations demonstrate controlled
heterogeneity. The string-price write should fail validation.
The collection metadata reveals the validator that actually
governs new writes. This does not prove all existing documents
satisfy the validator under every
validationLevel/validationAction;
those details are Chapter 07.
5. Deliberately wrong evolution: deploy the writer first
The most common schema incident is not MongoDB “accepting bad
data”; it is a deployment sequence that creates data some
still-running reader cannot understand. If a new writer changes
price from a scalar to an object before all readers
accept the object shape, old services can crash or miscompute
totals. The reverse can also happen during rollback if new-only
data has already been persisted.
The safer sequence is expand → migrate → contract: first make readers tolerant, then emit/backfill the new shape, observe version distributions and errors, then remove legacy support only after verification. Rollback planning must include data compatibility, not only reverting application binaries.
Production metrics should include counts by
schemaVersion, validator failures/warnings,
missing/incorrect types for critical fields, migration
throughput/errors, and reader decode failures. A flexible schema
is operationally mature when you can state which shapes exist
and why.
The next lesson closes the chapter with mongosh techniques to inspect documents and collection metadata directly instead of relying on application assumptions.
Cleanup
docker rm -f atlasmart-mongo-ch02-l4
Verification checklist
- The allowed schema versions and ownership of each representation are documented.
- Readers tolerate both old and new shapes before new writers are enabled.
- The validator rejects the intentionally wrong string-price document.
- Collection metadata confirms the validator actually installed on the server.
- Migration observability and rollback include persisted-data compatibility, not only application binaries.
Check your understanding
- Why does MongoDB flexible schema not mean “no schema”?
- What is the purpose of schemaVersion?
- Why should readers usually be upgraded before writers during a shape change?
- Is a validator enough to guarantee application compatibility?
- Why are null and missing important during migrations?
Review the answers
Applications, indexes, validators, encryption definitions, analytics pipelines, and consumers all depend on document fields/types. The server merely permits more heterogeneity unless you constrain it.
It makes the document contract explicit so readers, writers, migrations, and observability can distinguish allowed historical/current shapes.
During a rolling deployment, old readers may still be running. Tolerant readers first create a compatibility window in which new data can be introduced safely.
No. It can constrain server writes, but it cannot prove every reader, external API, analytics job, or event consumer understands the allowed shapes.
They can represent different domain/migration states. Collapsing them can erase whether a value is explicitly absent versus never written by older data.
Authoritative references
- MongoDB release notes — Official current stable server series and patch notes.
- MongoDB 8.3 release notes — Official 8.3 changes; 8.3.8 is the latest released patch at review time and 8.3.9 is upcoming.
- MongoDB Extended JSON v2 — Canonical and Relaxed Extended JSON representations and type-preservation rules.
- BSON types — Official BSON type definitions and ObjectId notes.
- PyMongo BSON data formats — Official Python driver mapping between Python dictionaries/types and BSON.
- Schema validation — Official overview of collection schema validation.
- Specify JSON Schema validation — Official MongoDB JSON Schema support and restrictions.
-
JSON Schema validation tips
— Official practical caveats such as
additionalProperties:falseand_id.