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.

Intermediate90–120 minutes$jsonSchema + validation-failure labMongoDB 8.3.8 · mongosh 2.10.0Last reviewed: September 2026

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.

01

Explain the difference between JSON Schema terminology and MongoDB BSON-aware $jsonSchema validation.

02

Write required-field, BSON-type, numeric-range, enumeration, nested-document, and array rules without confusing them with application serialization.

03

Locate already-invalid documents with the same candidate schema before enforcing it.

04

Interpret a validation failure as evidence about one attempted resulting document, not as proof that historical data is clean.

05

Recognize hard edge cases such as unsupported JSON Schema keywords and additionalProperties:false with the automatic _id field.

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

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. $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

bash · isolated Chapter 07 Lesson 1 lab setup
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}))' 
javascript · seed legacy data, define the schema, enforce it, and inspect failures
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

text · documented validation evidence
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.

javascript · controlled additionalProperties failure
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-bad before enforcement.
  • collMod succeeds 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.

bash · cleanup / full reset
docker rm -f atlasmart-mongo-ch07-l1docker volume rm atlasmart-mongo-ch07-l1-data

Check your understanding

  1. Why can an existing invalid document remain in a collection immediately after a strict validator is added?
  2. What does bsonType add beyond generic JSON Schema type?
  3. Why is $nor:[{{$jsonSchema:...}}] useful before collMod?
  4. Why can additionalProperties:false reject every normal insert if _id is omitted?
  5. 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

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.