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

Schema Validation with JSON Schema

Apply rules at insert/update time:

Valid insert:

javascript
db.products.insertOne({
  name: "USB-C Hub",
  sku: "HUB-01",
  price: NumberDecimal("49.99"),
  category: "accessory",
  inStock: true
})

Invalid insert (missing sku):

javascript
db.products.insertOne({
  name: "Bad Product",
  price: NumberDecimal("9.99"),
  category: "book"
})
// MongoServerError: Document failed validation

Add validator to existing collection

OptionMeaning
validationLevel: strictValidate inserts and updates
validationLevel: moderateValidate inserts + updates to valid docs only
validationAction: errorReject invalid writes
validationAction: warnLog 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)

bash
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:

javascript
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)

LimitTypical value
Duration60 second default max
Document size16 MB per doc unchanged
CollectionsCross-collection OK in same DB
Sharded clusterSupported with restrictions

Keep transactions short—no external HTTP calls inside.

Read/Write Concern with Transactions

javascript
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:

javascript
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

NeedTool
Field types, required keysSchema validator
Two collections update togetherTransaction
Idempotent retryUnique index + upsert
Long-running workflowSaga pattern in app, not long transaction

Post-Chapter Checklist

  • Created collection with $jsonSchema validator
  • 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.