Understanding MongoDB $jsonSchema Syntax
Within the broader MongoDB JSON Schema Validation Architecture, the $jsonSchema operator is the vocabulary you use to turn a flexible document store into a contract-enforced data platform. This guide is a complete syntax and deployment reference for platform teams, data engineers, and Python automation builders: it maps every keyword MongoDB actually honors, shows how the validator is evaluated on the synchronous write path, and gives you runnable pymongo and mongosh code to define, deploy, diagnose, and roll back a schema safely. Because $jsonSchema is evaluated inline on every insert, update, and replace, syntax precision here directly determines write latency, throughput, and how cleanly failures surface downstream. The deliverable by the end of the page is a schema you can express correctly the first time and a repeatable deployment function that never surprises production.
Architectural Context & Enforcement Boundaries
A $jsonSchema document is not a passive annotation — it is compiled into a query predicate that MongoDB runs against every candidate write before the document is committed to storage. The validator is attached to a collection through createCollection or collMod and wrapped in a validator: { $jsonSchema: … } object. When a write arrives, the engine parses the BSON payload, checks declared required keys, evaluates each property’s bsonType and value constraints, recurses into nested objects and array items, and short-circuits on the first violation to keep per-write CPU cost bounded.
This synchronous position is what makes syntax an operational concern rather than a stylistic one. $jsonSchema is the innermost enforcement boundary, and it pairs with the wider deployment machinery of collection-level validators that governs how the schema is applied. The operator only guards insert, update, replace, and findAndModify; the aggregation stages $out and $merge bypass the destination collection’s validator entirely, so any integrity contract that must survive analytics rewrites has to be reasserted through cross-collection validation patterns rather than assumed. Writes that fail can be diverted instead of dropped through fallback routing for invalid documents.
Prerequisites & Operational Requirements
The syntax and diagnostics below assume a supported production topology. Confirm the following before applying any schema to a live collection.
- MongoDB version: 5.0 or later. The structured
schemaRulesNotSatisfiedarray inside a validation error — the field that tells you which rule and JSON path failed — was introduced in 5.0. On 4.x you get onlycode: 121with a generic"Document failed validation"message and no path-level detail. - Driver: PyMongo 4.x (
pip install "pymongo>=4.6,<5"). Pin the driver in your automation image socollModargument handling and theWriteError/OperationFailureexception classes stay stable across builds. - Permissions: the deploying principal needs the
collModaction on the target collection, granted by the built-indbAdminrole. A dry-run compliance count additionally needsfind(read). - Topology: a replica set, not a standalone.
collModpropagates through the oplog, so schema changes are subject to replication lag on your slowest secondary, and you will want a maintenance window because applying a validator takes an exclusive collection lock. - Draft baseline: MongoDB implements JSON Schema Draft 4 semantics on every server version, including current releases. It omits
$ref,$schema,default,definitions,format, andid, and it rejects — rather than silently ignores — later-draft keywords such asif/then/else,$defs, andunevaluatedPropertiesatcollModtime. When you share schema artifacts with client tooling that defaults to a newer draft, reconcile the differences against JSON Schema Draft 4 vs Draft 2019 in MongoDB.
The two enforcement dials that accompany the schema are validationLevel and validationAction. Their combination decides which documents are checked and what happens on failure:
validationAction |
validationLevel |
Checks applied to | On failure |
|---|---|---|---|
warn |
moderate |
Inserts + updates to already-valid docs | Logs a warning; write succeeds |
warn |
strict |
All inserts and updates | Logs a warning; write succeeds |
error |
moderate |
Inserts + updates to already-valid docs | Rejects with WriteError code 121 |
error |
strict |
All inserts and updates | Rejects with WriteError code 121 |
moderate is the migration-safe choice when hardening a collection that predates the schema; the trade-offs are covered in depth under strict vs moderate validation levels.
Core Syntax & BSON Type Mapping
The single most important difference from standard JSON Schema is that MongoDB validates against BSON types, not JSON types. The bsonType keyword is the MongoDB extension that exposes the full type system; the standard type keyword also works but can only express the JSON subset. A schema is a nested object of constraints rooted at bsonType: "object".
const schema = {
$jsonSchema: {
bsonType: "object",
required: ["_id", "tenant_id", "event_type", "payload"],
additionalProperties: false,
properties: {
_id: { bsonType: "objectId" },
tenant_id: { bsonType: "string", minLength: 24, maxLength: 24 },
event_type: { enum: ["login", "transaction", "audit"] },
payload: {
bsonType: "object",
additionalProperties: false,
properties: {
amount: { bsonType: "decimal", minimum: 0 },
metadata: { bsonType: "object" }
}
},
created_at: { bsonType: "date" }
}
}
}
The bsonType values that have no plain-JSON equivalent are where most real bugs live. The mapping every schema author should keep in front of them:
bsonType |
Matches | Common mistake it prevents |
|---|---|---|
objectId |
12-byte ObjectId |
Accepting a 24-char hex string where an id is expected |
decimal |
Decimal128 |
Storing currency as double and inheriting float rounding |
int / long |
32-bit / 64-bit integer | A whole number silently stored as double |
date |
BSON Date |
An ISO-8601 string passing an unspecified field |
binData |
binary subtype | Base64 text where raw bytes belong |
timestamp |
internal BSON Timestamp |
Confusing it with date (they are distinct types) |
null |
the BSON null value | A missing key — which null does not match |
Operational notes that follow directly from the type system:
additionalProperties: falseis what turns a schema into a closed contract. Omit it and undeclared fields flow straight through, which is how uncontrolled drift enters aggregation pipelines and breaks downstream consumers.requiredvalidates only the keys you list, at whatever nesting level you declare it. It asserts presence, not type — pair every required key with apropertiesentry that constrains itsbsonType.- Numeric width is part of the contract. A schema demanding
bsonType: "double"rejects an integer literal because MongoDB stored it asint. When width is irrelevant, usebsonType: ["double", "int", "long", "decimal"]. - Validation cost scales with schema depth and
patterncomplexity. For high-throughput ingestion, keep the on-write schema shallow and push expensive structural checks to application-layer pre-validation.
Idempotent Deployment / Implementation Workflow
A production schema deployment must be deterministic: running it twice must not mutate collection metadata twice, and it must never take an exclusive lock it does not need. Follow this sequence from mongosh or its pymongo equivalent.
-
Inspect the current validator. It lives in the collection’s
options, retrieved throughlistCollections— not incollStats, which returns storage metrics only:db.getCollectionInfos({ name: "events" })[0].options.validator -
Count non-compliant documents with the schema as a query.
$jsonSchemais a valid query operator, so you can measure the blast radius before enforcing anything, with no validator active:db.events.countDocuments({ $nor: [ { $jsonSchema: schema.$jsonSchema } ] })A non-zero count means a
strictrollout would reject updates to those documents; usemoderateor clean the data first. -
Apply the validator in
warnfirst. This attaches the schema and starts emitting telemetry without rejecting writes, so you can watch real traffic against the contract:db.runCommand({ collMod: "events", validator: schema, validationLevel: "moderate", validationAction: "warn" }) -
Promote to
erroronce the rejection rate is acceptable. The promotion is a second, reversiblecollMod; nothing about the schema definition changes:db.runCommand({ collMod: "events", validationAction: "error" })
Conditional and array constraints slot into the same schema object. Because MongoDB is Draft 4, conditional requirements are expressed with the oneOf / anyOf / allOf combinators rather than if/then. Here oneOf demands stricter payload fields only when event_type is transaction:
const conditionalSchema = {
$jsonSchema: {
bsonType: "object",
required: ["event_type", "payload"],
oneOf: [
{
properties: {
event_type: { enum: ["transaction"] },
payload: {
bsonType: "object",
required: ["amount", "currency"],
properties: {
amount: { bsonType: "decimal", minimum: 0 },
currency: { bsonType: "string", pattern: "^[A-Z]{3}$" }
}
}
}
},
{ properties: { event_type: { enum: ["login", "audit"] } } }
]
}
}
Arrays are validated element-by-element through the items keyword, not as a monolithic block, which is where silent corruption creeps into event-sourced data. Always pair items with minItems / maxItems to bound growth; the deeper patterns are covered in validating nested arrays with $jsonSchema:
const arraySchema = {
$jsonSchema: {
bsonType: "object",
properties: {
line_items: {
bsonType: "array",
minItems: 1,
maxItems: 500,
items: {
bsonType: "object",
required: ["sku", "quantity"],
properties: {
sku: { bsonType: "string", pattern: "^[A-Z0-9]{8,12}$" },
quantity: { bsonType: "int", minimum: 1 }
}
}
}
}
}
}
Production-Ready Automation Implementation
Platform teams managing dozens of collections need schema deployment to be a function, not a console session. The pattern below applies a schema safely from pymongo: collMod has no dry-run flag, so it first exercises the schema on a throwaway namespace to catch syntax errors — including rejected non–Draft-4 keywords — before touching production, then applies it with explicit error boundaries and logging. Wiring this into a version-controlled registry is what makes it auditable; see schema versioning strategies for NoSQL.
import logging
import pymongo
from pymongo.errors import OperationFailure, ServerSelectionTimeoutError
logger = logging.getLogger("schema_governance")
def apply_json_schema(
client: pymongo.MongoClient,
db_name: str,
collection_name: str,
schema: dict,
validation_level: str = "moderate",
validation_action: str = "error",
) -> bool:
"""Deploy a $jsonSchema validator with a syntax dry-run and explicit error handling.
Step 1 creates a temporary collection with the proposed schema to verify syntax,
because collMod has no dry-run flag. Step 2 applies the validated schema to the
target collection via collMod. Returns True on success, False on a handled failure.
"""
db = client[db_name]
temp_name = f"__schema_dryrun_{collection_name}"
try:
# Step 1: syntax dry-run on a throwaway namespace.
db.drop_collection(temp_name)
try:
db.create_collection(
temp_name,
validator={"$jsonSchema": schema},
validationLevel=validation_level,
validationAction=validation_action,
)
logger.info("Dry-run passed for %s.%s", db_name, collection_name)
finally:
db.drop_collection(temp_name)
# Step 2: apply the validated schema to the production collection.
db.command(
"collMod",
collection_name,
validator={"$jsonSchema": schema},
validationLevel=validation_level,
validationAction=validation_action,
)
logger.info("Schema applied to %s.%s", db_name, collection_name)
return True
except OperationFailure as exc:
detail = exc.details.get("errmsg", str(exc)) if exc.details else str(exc)
logger.error("Schema deployment failed: %s", detail)
return False
except ServerSelectionTimeoutError as exc:
logger.critical("Cluster unreachable during schema deployment: %s", exc)
raise
Key automation practices:
- Always dry-run on a temporary collection first (Step 1). It is the only way to catch a syntax violation or an unsupported keyword without locking the production collection.
- Embed a
schema_versionfield in your documents so drift-detection and rollback scripts have a machine-readable anchor. - Use retryable writes and connection pooling in CI/CD pipelines so a transient primary election does not fail an otherwise valid deployment.
- Drive the deployment from infrastructure-as-code (Terraform, Ansible) so staging and production converge on the same schema idempotently.
Diagnostic Fingerprints & Fast Resolution
When a validator rejects a write, MongoDB returns a WriteError with code: 121 and errmsg: "Document failed validation". On 5.0+, the driver payload carries a details object whose schemaRulesNotSatisfied array isolates the exact failing rule and JSON path:
{
"code": 121,
"errmsg": "Document failed validation",
"errInfo": {
"failingDocumentId": "…",
"details": {
"operatorName": "$jsonSchema",
"schemaRulesNotSatisfied": [
{
"operatorName": "properties",
"propertiesNotSatisfied": [
{
"propertyName": "amount",
"details": [
{ "operatorName": "bsonType", "specifiedAs": { "bsonType": "decimal" }, "reason": "type did not match" }
]
}
]
}
]
}
}
}
In PyMongo this surfaces as pymongo.errors.WriteError for a single write, or BulkWriteError for a batch; inspect exc.details["errInfo"]["details"] to reach the structure above. A copy-paste diagnostic that reproduces the failing set from mongosh — no validator changes required:
// Show every document that would fail the schema, with the rule that broke.
db.events.find({ $nor: [ { $jsonSchema: schema.$jsonSchema } ] })
.limit(20).forEach(d => printjson({ _id: d._id, event_type: d.event_type }))
To confirm whether the failure is a schema problem versus a driver or keyword problem, apply the schema to a scratch collection and read the server’s own error text — an unsupported keyword surfaces as Unknown $jsonSchema keyword: <name> at collMod time, which is a portability bug, not a data bug.
Edge Cases, Gotchas & Known Limitations
- Later-draft keywords hard-fail.
if/then/else,$defs,unevaluatedProperties, and$refare rejected as unknown keywords atcollModtime. Translate them tooneOf/anyOf/allOfbefore deployment. nullis notmissing.bsonType: "null"matches a field whose value is BSON null; it does not satisfyrequiredfor an absent key, and it does not match a missing field. These are three distinct states.- Aggregation writes bypass validation.
$outand$mergedo not run the destination collection’s validator, so a pipeline can legally write documents your schema would reject. Re-validate or constrain the pipeline output explicitly. - Validators are not indexes.
$jsonSchemacannot enforce uniqueness — that requires a unique index. Schema validation catches malformed data; indexes enforce identity and optimize retrieval. They are complementary, not interchangeable. - Trusting the schema for authorization is a mistake. A validator constrains shape, not who may write; tenant isolation and field-level access belong to security boundaries in schema design.
- Deep schemas tax every write. Because evaluation is synchronous and recursive, a heavily nested schema with expensive
patternregexes adds measurable per-write latency under load. Profile withexplain()before shipping a complex schema to a hot collection.
Verification & Rollback Procedures
After a deployment, verify that the live validator matches what you intended and that new writes behave as expected:
// 1. Confirm the applied validator, level, and action.
db.getCollectionInfos({ name: "events" })[0].options
// 2. Confirm a known-bad document is now rejected (should throw code 121).
db.events.insertOne({ event_type: "transaction", payload: {} })
If the deployment misbehaves — an unexpected rejection spike or a downstream service that cannot yet meet the contract — rollback is a single, reversible collMod. Dropping to warn restores write availability instantly while keeping the schema attached for telemetry:
// Soft rollback: keep the schema, stop rejecting (time-to-recover: seconds).
db.runCommand({ collMod: "events", validationAction: "warn" })
// Hard rollback: remove the validator entirely.
db.runCommand({ collMod: "events", validator: {}, validationLevel: "off" })
Both commands are metadata-only and take effect immediately on the primary, propagating to secondaries through the oplog; time-to-recover for the soft rollback is effectively the replication lag of your slowest secondary. For authoritative operator behavior and edge cases, consult the official MongoDB $jsonSchema documentation.
Frequently Asked Questions
Which JSON Schema draft does MongoDB's $jsonSchema implement?
Draft 4 semantics on every server version, including current releases. It omits $ref, $schema, default, definitions, format, and id, and it rejects later-draft keywords such as if/then/else, $defs, and unevaluatedProperties at collMod time rather than ignoring them. Use oneOf, anyOf, and allOf for conditional logic.
What is the difference between bsonType and type?
type only expresses the JSON subset (string, number, object, array, boolean, null). bsonType is the MongoDB extension that reaches the full BSON type system — objectId, decimal, int, long, date, binData, timestamp. Prefer bsonType whenever type precision matters, which for stored data is almost always.
Why does a numeric field still fail bsonType: "double"?
Because MongoDB distinguishes numeric BSON types. An integer literal is stored as int (or long), not double, so a schema demanding "double" rejects it. When numeric width is not part of the contract, specify bsonType: ["double", "int", "long", "decimal"].
Can I test which existing documents fail a schema before enforcing it?
Yes. $jsonSchema is a valid query operator, so db.coll.countDocuments({ $nor: [ { $jsonSchema: <schema> } ] }) counts non-compliant documents with no validator active. This is how you size the blast radius of a strict rollout before committing to it.
Does additionalProperties: false apply to nested objects automatically?
No. It only closes the object at the level where you declare it. Nested objects remain open unless each one repeats additionalProperties: false. Undeclared fields on a nested object flow through silently otherwise, which is a common source of schema drift.
Related
- MongoDB JSON Schema Validation Architecture — the parent architecture this operator reference sits inside, spanning versioning, validation levels, and security.
- JSON Schema Draft 4 vs Draft 2019 in MongoDB — exactly which keywords MongoDB honors and how to port newer-draft schemas.
- Validating nested arrays with
$jsonSchema— element-levelitemspatterns that prevent silent array corruption. - Strict vs Moderate Validation Levels — how
validationLeveldecides which documents get checked during a rollout. - Implementing collection-level validators — the idempotent deployment machinery that applies these schemas in production.
- Schema versioning strategies for NoSQL — treating the schema as a version-controlled, auditable artifact.