Govern AtlasMart product documents with BSON-aware $jsonSchema validation, preflight invalid-data queries, nested rules, explicit failure evidence, and safe edge-case handling.
JSON Schema Validation with $jsonSchema: Required Fields, Types, Ranges, and Nested Rules
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.
Learning outcomes
AtlasMart has reached the point where “the application always
writes the right shape” is no longer credible. A bulk importer
created a negative price, a maintenance script omitted
status, and a new service is about to depend on a
nested inventory contract. A
collection validator is a server-side predicate
evaluated on writes. MongoDB's $jsonSchema query
predicate supplies a readable way to express document shape,
required fields, BSON types, ranges, nested-object rules, and
array rules. The validator is a data-integrity gate; it does not
migrate old documents, authorize users, or fill defaults.
Explain the difference between JSON Schema terminology and
MongoDB BSON-aware $jsonSchema validation.
Write required-field, BSON-type, numeric-range, enumeration, nested-document, and array rules without confusing them with application serialization.
Locate already-invalid documents with the same candidate schema before enforcing it.
Interpret a validation failure as evidence about one attempted resulting document, not as proof that historical data is clean.
Recognize hard edge cases such as unsupported JSON Schema
keywords and additionalProperties:false with
the automatic _id field.
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:27047, mongosh 2.10.0, and
PyMongo 4.17.0 where driver behavior is shown.
The database is atlasmart; the collection is
catalog_ch07_l1. 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.
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. $jsonSchema validates the resulting BSON document
BSON (Binary JSON) is MongoDB's typed document
representation. MongoDB supports a draft-4-derived subset of
JSON Schema plus the MongoDB-specific
bsonType keyword. That distinction matters: JSON
Schema's generic type:"number" cannot express every
BSON numeric type, while bsonType:"int",
"long", "decimal",
"objectId", and other BSON aliases can. MongoDB
does not support the JSON Schema type name integer;
use BSON integer types when integer representation itself is
part of the contract.
A validator is checked when an insert or update is subject to
validation. Adding a validator does not scan and rewrite
existing records. Under validationLevel:"strict", a
later update to an old invalid document must produce a document
that passes the current rule or the write is rejected. This
whole-document behavior is why a strict rule can surprise an old
service that changes an unrelated field.
| Rule | AtlasMart meaning | Important boundary |
|---|---|---|
required |
A field must exist in the object. |
Existence is different from allowing null;
type rules decide whether null is legal.
|
bsonType |
Restrict the BSON representation. |
Do not use type:"integer"; MongoDB documents
that JSON Schema integer is unsupported.
|
minimum/maximum |
Bound numeric values such as non-negative price. | A range rule does not prove a business price is correct; it only proves it is within the declared numeric range. |
enum |
Allow a finite state set such as active/discontinued. | Adding a new legitimate state is a compatibility change for old validators/readers. |
properties + nested schema |
Validate embedded documents recursively. | Nested required lists apply only within that object; reason carefully about whether the parent itself is required. |
items/uniqueItems |
Constrain array element shape and duplicate values. | Validation does not bound array growth unless you add explicit length constraints or redesign the aggregate. |
2. Preflight the candidate rule before enforcement
The safest first use of a candidate schema is often as a query
predicate. Because $jsonSchema can participate in
queries, AtlasMart can ask which documents do not match
the rule using $nor. That gives a measurable debt
count before collMod changes write behavior. It
also separates two questions that teams often conflate: “What
should new writes look like?” and “What historical data already
violates that contract?”
For a real rollout, stratify violations by cause rather than only counting them. Missing required fields may be backfillable; a type mismatch can indicate serializer drift; an impossible value can be genuine corruption that requires business review. A single total hides those repair paths.
3. Build the AtlasMart validator and observe failure evidence
docker rm -f atlasmart-mongo-ch07-l1 2>/dev/null || truedocker volume rm atlasmart-mongo-ch07-l1-data 2>/dev/null || truedocker run -d --name atlasmart-mongo-ch07-l1 \ -p 127.0.0.1:27047:27017 \ -v atlasmart-mongo-ch07-l1-data:/data/db \ mongodb/mongodb-community-server:8.3.8-ubuntu2204-slimmongosh "mongodb://127.0.0.1:27047/atlasmart?directConnection=true" --quiet --eval \'printjson({server:db.version(), hello:db.hello().isWritablePrimary}); printjson(db.getSiblingDB("admin").runCommand({getParameter:1,featureCompatibilityVersion:1}))'
const catalog = db.getCollection("catalog_ch07_l1");catalog.drop();catalog.insertMany([ { _id: "p-7001", schemaVersion: NumberInt(1), tenantId: "tenant-a", sku: "BOOK-001", name: "Distributed Data Systems", priceCents: NumberInt(4200), status: "active", inventory: { warehouse: "baku-1", qty: NumberInt(12) }, tags: ["books", "databases"] }, { _id: "p-legacy-bad", schemaVersion: NumberInt(1), tenantId: "tenant-a", sku: "BOOK-OLD", name: "Legacy import", priceCents: NumberInt(-50), inventory: { warehouse: "baku-1", qty: NumberInt(3) } }]);const productSchema = { bsonType: "object", required: ["_id", "schemaVersion", "tenantId", "sku", "name", "priceCents", "status", "inventory"], additionalProperties: false, properties: { _id: { bsonType: "string" }, schemaVersion: { bsonType: "int", enum: [1] }, tenantId: { bsonType: "string", minLength: 1 }, sku: { bsonType: "string", minLength: 1 }, name: { bsonType: "string", minLength: 1 }, priceCents: { bsonType: ["int", "long"], minimum: 0 }, status: { enum: ["active", "discontinued"] }, inventory: { bsonType: "object", required: ["warehouse", "qty"], additionalProperties: false, properties: { warehouse: { bsonType: "string" }, qty: { bsonType: ["int", "long"], minimum: 0 } } }, tags: { bsonType: "array", items: { bsonType: "string" }, uniqueItems: true } }};print("invalid before validator:");printjson(catalog.find({ $nor: [ { $jsonSchema: productSchema } ] }, { _id: 1, priceCents: 1, status: 1 }).toArray());printjson(db.runCommand({ collMod: "catalog_ch07_l1", validator: { $jsonSchema: productSchema }, validationLevel: "strict", validationAction: "error"}));try { catalog.insertOne({ _id: "p-bad-new", schemaVersion: NumberInt(1), tenantId: "tenant-a", sku: "BAD-1", name: "Negative price", priceCents: NumberInt(-1), status: "active", inventory: { warehouse: "baku-1", qty: NumberInt(2) } });} catch (e) { print("rejected:", e.code, e.codeName || e.name); printjson(e.errInfo?.details || {});}try { catalog.updateOne({ _id: "p-legacy-bad" }, { $set: { name: "Still invalid after rename" } });} catch (e) { print("legacy update rejected under strict validation:", e.code);}print("invalid after rejected writes:", catalog.countDocuments({ $nor: [ { $jsonSchema: productSchema } ] }));
Expected result shape
invalid before validator:[ { _id: 'p-legacy-bad', priceCents: -50 } ]{ ok: 1 }rejected: 121 DocumentValidationFailure{ operatorName: '$jsonSchema', schemaRulesNotSatisfied: [ ... ] }legacy update rejected under strict validation: 121invalid after rejected writes: 1
Error code 121 denotes document validation failure
in the normal server error path. The nested
errInfo detail is useful for diagnosis, especially
when description fields are present in the schema,
but MongoDB documents that rich error output is intended for
humans and can evolve. Application correctness should depend on
the write failure and stable error classification—not on parsing
one exact nested diagnostic shape.
4. Deliberately wrong: close the schema without accounting for _id
additionalProperties:false can be useful when
accidental fields are genuinely dangerous, but it turns every
omitted property declaration into a rejection. MongoDB documents
a particularly easy trap: every normal document has an
_id field, including automatically generated
identifiers. If additionalProperties:false is
enabled while _id is absent from
properties, otherwise sensible inserts are
rejected.
db.edge_ch07_l1.drop();db.edge_ch07_l1.insertOne({ _id: "edge-1", name: "existing" });const tooClosed = { bsonType: "object", required: ["name"], additionalProperties: false, properties: { name: { bsonType: "string" } }};printjson(db.runCommand({ collMod: "edge_ch07_l1", validator: { $jsonSchema: tooClosed } }));try { db.edge_ch07_l1.insertOne({ name: "auto id will exist" }); }catch (e) { print("auto-_id document rejected:", e.code); }// Repair: include _id in properties, or do not set additionalProperties:false until the contract is complete.
The repair is not “always allow everything.” The repair is to
enumerate the contract intentionally, include
_id when closing the object, and stage a
restrictive rule only after measuring real document diversity.
The same discipline applies to new fields introduced by an older
client during a rolling deployment.
5. Verification, cleanup, and production judgment
Verification checklist
-
The candidate query identifies
p-legacy-badbefore enforcement. -
collModsucceeds without rewriting the existing invalid record. - A new negative-price insert is rejected and reports document-validation failure.
- An unrelated update to the existing invalid document is rejected under strict validation.
- The invalid historical count remains one until a deliberate repair migration is executed.
- You can explain why a validator is a write-time integrity rule rather than a migration, defaulting system, or authorization boundary.
In production, validators are appropriate for invariants that the server can evaluate from the resulting document and that should hold regardless of which application writes. They add CPU to writes and can block deploys when the contract is tightened too early. They do not change read concern, write concern, replication durability, or cross-document atomicity. On replica sets/sharded clusters, rejected writes never become accepted application state, but rollout still has topology-wide operational consequences because every writer observes the collection rule. Treat validator changes like application API changes: observe, stage, test old/new clients, preserve rollback, and monitor rejection/log rates.
Security matters separately. A principal with
bypassDocumentValidation can use supported write
commands to bypass validation; an unauthenticated disposable lab
has no meaningful authorization boundary. Production application
roles should not receive bypass privileges casually. Validators
also do not isolate tenants: tenant authorization must be
enforced by application/role/network design.
The next lesson turns this static rule into a rolling-deployment
problem: how strict/moderate and
error/warn change what old documents
and old clients can do.
docker rm -f atlasmart-mongo-ch07-l1docker volume rm atlasmart-mongo-ch07-l1-data
Check your understanding
- Why can an existing invalid document remain in a collection immediately after a strict validator is added?
-
What does
bsonTypeadd beyond generic JSON Schematype? -
Why is
$nor:[{{$jsonSchema:...}}]useful beforecollMod? -
Why can
additionalProperties:falsereject every normal insert if_idis omitted? - Why should application code not parse the exact nested text of validation diagnostics as a stable API?
Review the answers
Validation is applied to qualifying inserts/updates; adding the rule does not retroactively rewrite or automatically reject stored documents. The old document is checked when a future operation subjects it to validation.
bsonType can constrain MongoDB-specific BSON
representations such as int, long, decimal, objectId,
date, and binary. That is more precise than the generic
JSON type vocabulary.
It turns the candidate contract into an observability query, so you can count and inspect historical debt before changing write behavior.
Every document has _id. A closed object says
fields not declared in properties are
forbidden, so _id itself becomes an
undeclared property unless you include it.
MongoDB documents that detailed validation error output is intended for human consumption and may evolve. Depend on stable error classification and your own acceptance tests, not one serialized diagnostic tree.
Authoritative references
- MongoDB 8.3 release notes — Current server-series and patch-release status; re-check before reproducing the lab.
- MongoDB versioning — Explains major/minor release series and why the latest stable patch should be used.
- mongosh release notes — Current mongosh release and version-sensitive shell behavior.
- PyMongo release notes — Current Python driver line used when application-side migration behavior is demonstrated.
- Schema validation overview — Official entry point for collection validation behavior.
- Specify JSON Schema validation — Official examples and restrictions for JSON Schema collection validation.
- $jsonSchema operator — Supported draft-4 keywords, BSON extension, omissions, and query syntax.
-
JSON Schema validation tips
— Documents the
additionalProperties:falseand_idedge case. -
Query valid or invalid documents
— Shows using
$jsonSchemain query conditions to locate noncompliant documents. -
Privilege actions
— Defines the
bypassDocumentValidationprivilege action.