Schema validation
MongoDB is schema-flexible by default: a collection will happily accept documents of wildly different shapes, which is freeing during prototyping and dangerous in production. Schema validation lets you opt back into enforcement — to declare, for a given collection, which fields must exist, what types they must be, and what values are allowed — without giving up the document model’s flexibility where you still want it. You attach the rules with a $jsonSchema validator, and from then on the server checks every write against them. It is the safety rail you add once a collection’s shape has settled.
What a validator looks like
Section titled “What a validator looks like”A validator is a query expression that every document must satisfy. The $jsonSchema operator is the standard way to write one, built from three core keywords:
bsonType— the type a field (or the document) must be, such as"object","string","int", or"array".required— an array of field names that must be present.properties— a per-field map describing each field’s type and constraints.
Here is a validator for a members collection: every member must have a string name and an integer joined year of at least 1900, and if fines is present it must be a non-negative number.
flowchart LR
W["Incoming write"] --> V{"Matches $jsonSchema?"}
V -->|"Yes"| A["Accepted and stored"]
V -->|"No, action error"| R["Rejected with a validation error"]
V -->|"No, action warn"| L["Stored but logged as a warning"] Creating a collection with a validator
Section titled “Creating a collection with a validator”Attaching a validator is part of creating the collection. The call below builds members with the rules described above. This is a real operation, so it looks slightly different in the shell and in each driver:
db.createCollection("members", { validator: { $jsonSchema: { bsonType: "object", required: ["name", "joined"], properties: { name: { bsonType: "string" }, joined: { bsonType: "int", minimum: 1900 }, fines: { bsonType: "number", minimum: 0 } } } }, validationLevel: "strict", validationAction: "error"})await db.createCollection("members", { validator: { $jsonSchema: { bsonType: "object", required: ["name", "joined"], properties: { name: { bsonType: "string" }, joined: { bsonType: "int", minimum: 1900 }, fines: { bsonType: "number", minimum: 0 }, }, }, }, validationLevel: "strict", validationAction: "error",});db.create_collection( "members", validator={ "$jsonSchema": { "bsonType": "object", "required": ["name", "joined"], "properties": { "name": {"bsonType": "string"}, "joined": {"bsonType": "int", "minimum": 1900}, "fines": {"bsonType": "number", "minimum": 0}, }, } }, validationLevel="strict", validationAction="error",)schema := bson.M{ "$jsonSchema": bson.M{ "bsonType": "object", "required": bson.A{"name", "joined"}, "properties": bson.M{ "name": bson.M{"bsonType": "string"}, "joined": bson.M{"bsonType": "int", "minimum": 1900}, "fines": bson.M{"bsonType": "number", "minimum": 0}, }, },}opts := options.CreateCollection(). SetValidator(schema). SetValidationLevel("strict"). SetValidationAction("error")err := db.CreateCollection(ctx, "members", opts)if err != nil { return err}let validator = doc! { "$jsonSchema": { "bsonType": "object", "required": ["name", "joined"], "properties": { "name": { "bsonType": "string" }, "joined": { "bsonType": "int", "minimum": 1900 }, "fines": { "bsonType": "number", "minimum": 0 } } }};db.create_collection("members") .validator(validator) .validation_level(ValidationLevel::Strict) .validation_action(ValidationAction::Error) .await?;A document that satisfies every rule is stored without comment:
{ "name": "Ada", "joined": 2024, "fines": 0 }A document that breaks a rule is rejected. This one is missing the required joined field and gives name the wrong type, so the write fails with a validation error rather than being silently stored:
{ "name": 42 }validationLevel and validationAction
Section titled “validationLevel and validationAction”Two knobs control how the rules bite. validationLevel decides which writes are checked: "strict" (the default) validates every insert and update, while "moderate" validates inserts and updates to documents that already satisfy the schema but leaves existing invalid documents alone — useful when you add validation to a collection that already holds messy data. validationAction decides what happens on failure: "error" (the default) rejects the write, while "warn" allows it but records a warning in the server log — a gentle way to discover violations without breaking writes during a migration.
A common rollout is to begin with validationAction: "warn" and validationLevel: "moderate", watch the logs to learn how much existing data violates the new rules, fix the offenders, then tighten to "error" and "strict".
In Compass: open a collection and go to the Validation tab. You can paste or edit a $jsonSchema document there, pick the validation level and action from dropdowns, and Compass will preview which existing documents would pass or fail before you save the rules — a safe way to test a validator against real data.
Tips and gotchas
Section titled “Tips and gotchas”- Validation is opt-in and per-collection. A collection with no validator behaves exactly as before — nothing is enforced.
bsonTypeis stricter than JSON’s loose number handling:"int","long","double", and"decimal"are distinct. If a driver sends a value as a double where you required an int, the write fails — a frequent surprise.requiredonly checks that a field is present; the per-field rules inpropertiescheck its type and value. Use both together to fully pin a field down.- Use
"moderate"plus"warn"when adding validation to an existing collection so you do not break writes to documents that predate the rules. - You can change or remove a validator later with the
collModcommand — validation is not a one-way door.