Schema Validation and Transactions
Introduction
MongoDB collections are schema-flexible, but production apps still need rules: required fields, type checks, and multi-document ACID transactions for transfers and inventory. This chapter adds collection validators with JSON Schema, runs transactions in mongosh (replica set required), and briefly covers change streams for reactive pipelines.
Prerequisites
- BSON and Document Basics
- Insert and Import Export
- Aggregation Pipeline Basics
- MySQL transaction concepts — Transactions basics
Schema Validation with JSON Schema
Apply rules at insert/update time:
Valid insert:
db.products.insertOne({
name: "USB-C Hub",
sku: "HUB-01",
price: NumberDecimal("49.99"),
category: "accessory",
inStock: true
})Invalid insert (missing sku):
db.products.insertOne({
name: "Bad Product",
price: NumberDecimal("9.99"),
category: "book"
})
// MongoServerError: Document failed validationAdd validator to existing collection
| Option | Meaning |
|---|---|
validationLevel: strict | Validate inserts and updates |
validationLevel: moderate | Validate inserts + updates to valid docs only |
validationAction: error | Reject invalid writes |
validationAction: warn | Log warning, allow write (dev only) |
Tip
Validate in App and DB
Use Pydantic / Mongoose in the API for user-friendly errors; use MongoDB validator as a safety net against buggy clients — PyMongo chapter.
Transactions Overview
MongoDB multi-document transactions (4.0+) provide ACID semantics similar to MySQL transactions:
- Atomicity — all or nothing
- Consistency — constraints hold (including validators)
- Isolation — snapshot isolation
- Durability — depends on write concern
Requirement: deployment must be a replica set (including single-node replica set for dev). Standalone mongod without replica set cannot run transactions in modern versions.
Single-node replica set for local dev (Docker)
docker run -d --name mongo-rs \
-p 27017:27017 \
mongo:7 mongod --replSet rs0 --bind_ip_all
docker exec -it mongo-rs mongosh --eval 'rs.initiate()'Wait until rs.status() shows primary.
Transaction in mongosh
Transfer inventory between two product documents:
Every operation in the transaction must pass { session }.
Seed stock if testing:
db.products.insertMany([
{ name: "Hub", sku: "HUB-01", price: NumberDecimal("49.99"), category: "accessory", stock: 10 },
{ name: "Case", sku: "CASE-01", price: NumberDecimal("19.99"), category: "accessory", stock: 0 }
])Transaction Limits (Know the Bounds)
| Limit | Typical value |
|---|---|
| Duration | 60 second default max |
| Document size | 16 MB per doc unchanged |
| Collections | Cross-collection OK in same DB |
| Sharded cluster | Supported with restrictions |
Keep transactions short—no external HTTP calls inside.
Read/Write Concern with Transactions
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" }
})Production replica sets use w: "majority" for durability — Replica sets.
Change Streams (Optional)
Watch collection for inserts/updates/deletes:
const changeStream = db.posts.watch(
[{ $match: { operationType: { $in: ["insert", "update"] } } }]
)
changeStream.hasNext()
changeStream.next()Use cases:
- Invalidate Redis cache on write — Redis caching
- Sync search index
- Audit log consumer
Requires replica set. Resume with resumeAfter token after disconnect.
Compare Redis Pub/Sub — fire-and-forget vs durable oplog tail — Redis Pub/Sub.
Validation vs Transactions Decision
| Need | Tool |
|---|---|
| Field types, required keys | Schema validator |
| Two collections update together | Transaction |
| Idempotent retry | Unique index + upsert |
| Long-running workflow | Saga pattern in app, not long transaction |
Post-Chapter Checklist
- Created collection with
$jsonSchemavalidator - Saw validation error on bad insert
- Initialized replica set for dev (if testing transactions)
- Ran commit/abort transaction with
session - You know change streams require replica set
FAQ
Transactions on Atlas free tier?
Yes—Atlas runs replica sets; transactions work on M0 within limits.
Validator on existing bad data?
moderate level skips invalid legacy docs until updated.
Same as SQL CHECK constraint?
Similar intent; JSON Schema is richer for nested documents.
PyMongo transaction API?
with client.start_session() as session: + session.with_transaction(callback) — chapter 19.
Change stream vs polling?
Change streams push from oplog; polling wastes queries and adds lag.