JSON Schema Draft 4 vs Draft 2019 in MongoDB: Which Keywords Survive collMod?
MongoDB’s $jsonSchema validator implements JSON Schema Draft 4 on every server version, yet the client-side libraries most teams use for pre-flight checks — Python’s jsonschema, for instance — default to Draft 2019-09 or later, and reconciling that gap is the exact problem this page solves. It sits under Understanding MongoDB $jsonSchema Syntax within the broader MongoDB JSON Schema Validation Architecture, and the outcome you get here is a keyword-by-keyword map of what MongoDB honors, the precise collMod errors a later-draft schema throws, and a runnable playbook to migrate a shared schema artifact without stalling ingestion. This is not a cosmetic syntax difference: the draft you target changes which keywords exist, how conditional constraints must be written, and whether a validator even deploys.
The divergence matters most when the same schema definition is reused across two enforcement layers — client-side jsonschema running Draft 2019-09 against the full application payload, and server-side $jsonSchema running Draft 4 on the synchronous write path. A keyword that passes the client validator can hard-fail collMod, so migrating drafts safely intersects directly with schema versioning strategies for NoSQL and with fallback routing for invalid documents.
Operational Mechanics and Write-Path Impact
Draft 4 uses a flat, iterative keyword matcher. Arrays validate through positional items tuples bounded by additionalItems; cross-field constraints are expressed with dependencies; schema identity uses a bare id. Draft 2019-09 replaces this with a recursive evaluation model that tracks which properties and array indices were already evaluated, introduces $defs for reusable subschemas, and splits older keywords into narrower ones. Because MongoDB never advanced past Draft 4, every one of those newer keywords is rejected as unknown at collMod time rather than silently ignored — the validator refuses to deploy.
The practical mapping every migration must apply before a Draft 2019-09 artifact can run server-side:
| Draft 2019-09 keyword | MongoDB Draft 4 equivalent | Migration action |
|---|---|---|
$defs + $ref |
(none — no reference resolution) | Inline every referenced subschema into the validator |
dependentRequired / dependentSchemas |
dependencies |
Rewrite as a single dependencies object |
unevaluatedProperties |
additionalProperties |
Close each object level explicitly with additionalProperties: false |
unevaluatedItems |
additionalItems (or single-schema items) |
Use positional items + additionalItems: false, or one items schema |
if / then / else |
oneOf / anyOf / allOf |
Re-express conditionals as combinators |
$schema |
(none) | Strip the meta-schema declaration entirely |
prefixItems (Draft 2020-12, not 2019-09) |
positional items array |
Convert tuple validation to items: [ … ] |
Two facts anchor the whole migration. First, prefixItems is a Draft 2020-12 keyword — in Draft 2019-09 tuple validation still uses items as an array of schemas, so do not expect it in a 2019-09 artifact. Second, unevaluatedProperties is not just unsupported but semantically heavier than its replacement: it runs after properties, patternProperties, and additionalProperties have all been applied, so even where a client validator supports it, it costs more per document than the additionalProperties: false you must fall back to on the server. The trade-off between checking every document and only newly-modified ones is governed separately by strict vs moderate validation levels.
Array handling is where silent corruption creeps in during a downgrade. A Draft 2019-09 schema that used unevaluatedItems: false to forbid extra elements must become either a single items schema applied to all elements, or a positional items array paired with additionalItems: false; the element-level patterns are covered in validating nested arrays with $jsonSchema.
Exact Diagnostic Fingerprints and Fast Resolution
Draft-mismatch failures surface as a small set of precise signatures. The server rejects unsupported keywords at deploy time and rejects non-compliant documents at write time with WriteError code 121.
| Error signature | Root cause | Resolution |
|---|---|---|
Unknown $jsonSchema keyword: unevaluatedProperties (at collMod) |
Later-draft keyword applied to a Draft 4 validator | Replace with additionalProperties: false at that object level |
Unknown $jsonSchema keyword: $ref (at collMod) |
Reference resolution does not exist server-side | Inline the referenced subschema; keep $ref only in the client copy |
Unknown $jsonSchema keyword: if (at collMod) |
Conditional applicators are Draft 7+ | Rewrite as oneOf / anyOf / allOf |
Document failed validation (WriteError code 121) |
A document violates the deployed schema | Inspect errInfo.details.schemaRulesNotSatisfied for the failing path |
| Documents pass the client validator but fail on write | Client draft accepts a shape Draft 4 rejects (or vice versa) | Diff the two schemas; the server contract is authoritative |
The fastest way to confirm a keyword problem versus a data problem is to apply the candidate schema to a throwaway namespace and read the server’s own error text — an unsupported keyword fails immediately, before any document is written:
from pymongo import MongoClient
from pymongo.errors import OperationFailure
client = MongoClient("mongodb://localhost:27017")
db = client["appdb"]
candidate = {
"bsonType": "object",
"required": ["order_id"],
"unevaluatedProperties": False, # Draft 2019-09 keyword MongoDB will reject
"properties": {"order_id": {"bsonType": "string"}},
}
db.drop_collection("__draft_probe")
try:
db.create_collection("__draft_probe", validator={"$jsonSchema": candidate})
print("OK: schema is Draft 4 compatible")
except OperationFailure as exc:
print("REJECTED:", exc.details.get("errmsg", str(exc)))
finally:
db.drop_collection("__draft_probe")
Expected output for the schema above:
REJECTED: Unknown $jsonSchema keyword: unevaluatedProperties
Because $jsonSchema is also a valid query operator, you can size the blast radius of a draft downgrade against real data before enforcing anything — this counts documents the ported Draft 4 schema would reject, with no validator active:
// mongosh — draft4Schema is your inlined, keyword-mapped version
db.orders.countDocuments({ $nor: [ { $jsonSchema: draft4Schema } ] })
Step-by-Step Playbook
The migration path below downgrades a Draft 2019-09 artifact to a Draft 4 validator and deploys it without rejecting live writes. Each step is reversible.
-
Port the keywords. Strip
$schemaand$ref, inline$defs, and rewriteif/then/elseas combinators andunevaluatedPropertiesasadditionalProperties: false. Verify the ported schema on a scratch namespace using the probe from the previous section — it must printOKbefore you proceed. -
Count non-compliant documents. Run the
$nor+$jsonSchemacount above. A non-zero result means astrictcutover would reject updates to those documents, so plan amoderaterollout or clean the data first. -
Deploy in
warnto collect telemetry. Attach the ported schema without rejecting any write:db.runCommand({ collMod: "orders", validator: { $jsonSchema: draft4Schema }, validationLevel: "moderate", validationAction: "warn" })Expected result:
{ ok: 1 }. Non-compliant writes now emit a warning to the server log (message id20294) instead of failing, letting a Change Stream consumer route offending payloads to a dead-letter queue. -
Promote to
erroronce warnings settle. When the warning rate drops below your threshold, flip the action — the schema definition itself does not change:db.runCommand({ collMod: "orders", validationAction: "error" })Expected result:
{ ok: 1 }. New writes that violate the Draft 4 contract now fail withWriteErrorcode 121.
For high-throughput ingestion, keep the client-side jsonschema pre-flight in place using jsonschema.Draft201909Validator against the richer schema, and let the server enforce the flattened Draft 4 equivalent — this shifts most validation cost to stateless application nodes. Wiring the two definitions to a single version-controlled source is the job of schema versioning strategies for NoSQL.
Failure Modes & Rollback
| Failure at step | Symptom | Recovery |
|---|---|---|
| Step 1 — incomplete port | Unknown $jsonSchema keyword on collMod |
Re-run the probe; map the named keyword and retry (no production impact — the scratch namespace absorbs it) |
Step 3 — warn masks a broken port |
Silent flood of log 20294 warnings |
Read the warnings, fix the schema, re-apply; writes were never blocked |
Step 4 — error cutover too early |
Ingestion 121 rejection spike | Soft-rollback to warn immediately (below); time-to-recover is seconds |
The soft rollback keeps the schema attached for telemetry but stops rejecting writes; the hard rollback detaches the validator entirely:
// Soft rollback — restore write availability, keep the contract observable.
db.runCommand({ collMod: "orders", validationAction: "warn" })
// Hard rollback — remove the validator completely.
db.runCommand({ collMod: "orders", validator: {}, validationLevel: "off" })
Both commands are metadata-only, take effect immediately on the primary, and propagate through the oplog; time-to-recover for the soft rollback is effectively the replication lag of your slowest secondary. Because collMod takes an exclusive collection lock while it swaps validator metadata, run cutovers and rollbacks in a low-throughput window and confirm the applied state afterward:
db.getCollectionInfos({ name: "orders" })[0].options
Frequently Asked Questions
Can I set $schema to Draft 2019-09 to make MongoDB use that draft?
No. MongoDB does not read $schema at all — it implements Draft 4 unconditionally, and declaring $schema inside a validator fails at collMod as an unknown keyword. Strip it before deployment and keep it only in the client-side artifact that jsonschema consumes.
Is prefixItems supported for tuple validation?
No — prefixItems is a Draft 2020-12 keyword, and MongoDB is Draft 4. Even a Draft 2019-09 client schema would not use it; 2019-09 expresses tuples as items given an array of schemas. Server-side, use positional items plus additionalItems: false.
Why does a schema that passes my Python jsonschema check still fail on write?
Because the two layers run different drafts. A Draft 2019-09 client validator can accept a shape that the server's Draft 4 $jsonSchema rejects (or vice versa, when a ported keyword narrows a constraint). Treat the deployed server validator as authoritative and diff the two definitions from a single versioned source.
Related
- Understanding MongoDB
$jsonSchemaSyntax — the parent syntax and deployment reference this draft comparison sits under. - MongoDB JSON Schema Validation Architecture — the enforcement architecture spanning versioning, validation levels, and security.
- Validating nested arrays with
$jsonSchema— portingunevaluatedItemsto Draft 4itemswithout losing element-level checks. - Strict vs Moderate Validation Levels — choosing which documents get checked during a draft migration.
- Schema versioning strategies for NoSQL — keeping the client and server schema definitions reconciled from one versioned source.