Schema Validation: Data Validation at the Database Layer
Schema validation is data validation at the database layer—it can filter out invalid data even without an application layer.
The Unique Value of Database-Level Validation: Why is schema validation still necessary even with Mongoose validators? Because Mongoose validation only takes effect at the application layer— 1. Multi-application integration: If multiple microservices (Node.js/Python/Go) write to the same MongoDB database, but only the Node.js service uses Mongoose validation while the others do not; 2. Direct database operations: Operations teams using the Mongo shell to repair data or ETL scripts writing directly to the database bypass Mongoose; 3. Defense in depth: Even if there are bugs in application-layer validation, the database layer can still intercept them. Schema Validation serves as the last line of defense—it is not triggered under normal circumstances (since the application layer has already intercepted issues), but it protects data integrity in exceptional situations.
Limitations and Workarounds for Schema Validation: MongoDB Schema Validation has clear limitations—1. It does not support cross-field validation (e.g., "endDate > startDate"), which must be handled at the application layer; 2. It does not support asynchronous validation (e.g., "username must be unique," which requires a database lookup), which must be handled using a unique index; 3. It does not support conditional validation (e.g., “author is required when type='book'”), which must be handled by the application layer; 4. $jsonSchema does not support all MongoDB operators (e.g., $regex is restricted). Therefore, Schema Validation cannot fully replace application-layer validation—the correct approach is for the application layer to handle comprehensive validation (user-friendly error messages, cross-field logic, asynchronous validation), while the database layer handles fallback validation (required fields, data types, ranges, and uniqueness).
1. What You'll Learn
- $jsonSchema validator
- validator action(error / warn)
- validationLevel(strict / moderate)
- mongoose Schema vs MongoDB $jsonSchema Comparison
- Schema Evolution Strategy
graph LR
A[Client-Side Document Insertion] --> B{mongo<br/>Schema Validation}
B -->|validationLevel<br/>strict/moderate| C{Validation Rules}
C -->|bsonType| D[Type Checking]
C -->|required| E[Required Checks]
C -->|pattern| F[Regular Expression Validation]
C -->|enum| G[Enumeration Check]
C -->|minLength| H[Length Check]
D --> I{Through?}
E --> I
F --> I
G --> I
H --> I
I -->|Yes + action=error| J[✅ Insertion successful]
I -->|No + action=error| K[❌ Reject + Throw error]
I -->|No + action=warn| L[⚠️ Allow + Warning]
style J fill:#d4edda
style K fill:#f8d7da
2. $jsonSchema Validator
Concept Explanation: $jsonSchema is a document schema validation language introduced in MongoDB 3.6+ that is based on the JSON Schema specification. It allows you to define structural rules that documents must satisfy at the database level—such as field types, required fields, value ranges, and regular expression patterns. Unlike application-layer validation, $jsonSchema is enforced by the MongoDB engine, and any client (Python, Java, Node.js) writing data must comply with it.
How It Works: When creating a collection with a validator, MongoDB stores the $jsonSchema rules in the collection metadata. For every insert or update operation, the engine automatically validates whether the document meets the rules before writing it. If validation fails, the system determines whether to throw an error and reject the operation or log a warning based on the validationAction.
$jsonSchema Core Keywords:
| Keyword | Function | Example |
|---|---|---|
bsonType |
Specify BSON type | 'string', 'int', 'object', 'array' |
required |
List of Required Fields | ['email', 'username'] |
properties |
Field-Level Rule Definition | { email: { bsonType: 'string' } } |
pattern |
Regular Expression Validation | '^.+@.+$' (Email Format) |
enum |
Enumeration value | ['customer', 'admin'] |
minimum / maximum |
Value Range | minimum: 0, maximum: 150 |
minLength / maxLength |
String Length | minLength: 3, maxLength: 30 |
items |
Rules for Array Elements | { bsonType: 'string' } |
minItems |
Minimum Array Length | minItems: 1 |
The Relationship Between $jsonSchema and JSON Schema: MongoDB’s $jsonSchema is based on the JSON Schema Draft 4 specification, but there are several key differences— 1. It uses bsonType instead of type (because JSON Schema does not distinguish between BSON types such as int, double, decimal, and objectId); 2. additionalProperties is set to true by default (allowing undefined fields, which differs from the default in JSON Schema Draft 4); 3. It does not support $ref references (all rules must be defined inline); 4. It does not support format (e.g., email, uri, date-time; regular expressions must be used instead). Understanding these differences helps avoid the confusion of "copying code from a JSON Schema tutorial verbatim only to encounter errors."
Nested Validation with $jsonSchema: $jsonSchema supports recursive validation of nested objects and arrays—1. For nested objects, define the sub-structure using properties and required (e.g., address: {bsonType: 'object', required: ['city'], properties: {city: {bsonType: 'string'}}}); 2. For arrays, use items to define element rules (e.g., tags: {bsonType: 'array', items: {bsonType: 'string'}} to validate that all array elements are strings); 3. There is no strict limit on nesting depth, but excessive nesting can impact validation performance and readability—consider splitting into separate collections for more than three levels of nesting. Nested validation is a core advantage of the document model—while SQL requires multi-table JOINs to validate related data, MongoDB validates the entire document tree in a single pass.
Use Cases:
- Protection of critical data structures (users, orders, payment records)
- Unified Validation for Shared Databases in Multilingual Microservices
- Prevent dirty data from being written (as a fallback in case of missed application-layer validation)
- Not suitable for: validation requiring asynchronous processing, validation across multiple collections, or validation involving complex business logic
The Relationship Between the $jsonSchema Specification and JSON Schema: MongoDB’s $jsonSchema is based on the JSON Schema Draft 4 specification but includes BSON extensions—replacing type with bsonType (since MongoDB uses BSON rather than JSON for types) and adding BSON-specific types such as objectId, decimal, and date. Understanding this relationship is important: 1. bsonType: 'string' corresponds to JSON Schema’s type: 'string'; 2. bsonType: 'int' has no direct counterpart (JSON only has number); 3. required, properties, pattern, and enum are identical to those in JSON Schema.
Version Migration Strategy: Changes to Schema Validation require a versioning strategy—1. Adding optional fields: Low risk; deploy directly with “moderate” + “warn”; 2. Adding required fields: Medium risk; first set to “optional” → perform data migration → then set to “required”; 3. Changing field types: High risk; dual-write fields → migrate → switch → delete old fields; 4. Narrowing value ranges: Medium risk—first observe with “moderate” and “warn” → confirm no widespread violations → switch to “error.” Record the old rules for each change; roll back using collMod if necessary.
// === Create a collection with validation ===
db.createCollection('users', {
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['email', 'username'],
properties: {
email: {
bsonType: 'string',
pattern: '^.+@.+$',
maxLength: 100
},
username: {
bsonType: 'string',
minLength: 3,
maxLength: 30
},
age: {
bsonType: 'int',
minimum: 0,
maximum: 150
},
role: {
enum: ['customer', 'admin', 'moderator']
},
isActive: {
bsonType: 'bool'
}
}
}
},
validationLevel: 'strict',
validationAction: 'error'
});
Key Points Analysis:
bsonTypeUnlike JSON Schema’stype, MongoDB uses BSON type names (such as'int'rather than'number')requiredis a top-level keyword whose value is an array of field names; it does not belong to any property.- Nested documents are defined using
properties, and array elements are defined usingitems
Design Patterns for Nested Validation: There are three design patterns for nested validation in $jsonSchema: 1. Fully Inline Pattern (where address and item are directly nested within the $jsonSchema of the order; this provides a clear structure but results in verbose code); 2. Variable Extraction Pattern (where addressSchema and itemSchema are defined as JavaScript variables and referenced in the main schema; this offers good code reusability but requires management at the application layer); 3. Hybrid Mode (core fields are inlined, while reusable sub-structures are extracted as variables). Mode 3 is recommended for production environments—since addresses and order items may be reused across multiple collections (both orders and users have addresses), extracting them as independent variables reduces redundant definitions.
Edge Cases in Array Validation: There are several error-prone edge cases in $jsonSchema array validation—1. minItems and maxItems check the array length rather than the number of documents (an empty array [] passes minItems: 0 but fails minItems: 1); 2. items defines the rules for all elements (it does not support tuple validation where “the first 3 elements are of different types”; JSON Schema Draft 4 supports this, but MongoDB does not); 3. uniqueItems: true checks for uniqueness of array elements but may not work as expected with nested objects (objects are compared by reference rather than by depth); 4. Empty array vs. null array—an empty array [] passes validation with bsonType: 'array', but null does not (requires bsonType: ['array', 'null'] to allow null).
▶ Example 1: Nested Documents + Array Validation
// ShopHub Order Collection:Nested Addresses + Validation of the Order Items Array
db.createCollection('orders', {
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['userId', 'items', 'total', 'address'],
properties: {
userId: { bsonType: 'objectId' },
items: {
bsonType: 'array',
minItems: 1,
items: {
bsonType: 'object',
required: ['productId', 'qty', 'price'],
properties: {
productId: { bsonType: 'objectId' },
qty: { bsonType: 'int', minimum: 1 },
price: { bsonType: 'decimal', minimum: 0 }
}
}
},
address: {
bsonType: 'object',
required: ['street', 'city', 'zipCode'],
properties: {
street: { bsonType: 'string', minLength: 1 },
city: { bsonType: 'string' },
zipCode: { bsonType: 'string', pattern: '^[0-9]{5,10}$' }
}
},
total: { bsonType: 'decimal', minimum: 0 }
}
}
},
validationLevel: 'moderate',
validationAction: 'error'
});
// Test:Valid Orders
db.orders.insertOne({
userId: ObjectId(),
items: [{ productId: ObjectId(), qty: Int32(2), price: Decimal128('29.99') }],
address: { street: '123 Main St', city: 'Seattle', zipCode: '98101' },
total: Decimal128('59.98')
});
// ✅ Success
// Test: Empty items Array
db.orders.insertOne({
userId: ObjectId(),
items: [],
address: { street: '123 Main St', city: 'Seattle', zipCode: '98101' },
total: Decimal128('0')
});
// ❌ Document failed validation (minItems: 1)
3. validationAction
Concept Description: validationAction Controls MongoDB’s behavior when a validation check fails—whether to strictly reject the operation (error) or to allow it with a warning (warn). This is a critical trade-off between data integrity and business continuity.
How It Works:
errorMode: Throws aDocumentFailedValidationerror when validation fails; the write operation is rolled back, and the client receives an exception.warnMode: Writes still succeed even if validation fails, but a warning message is logged in themongodlog; suitable for gradual rollout
The Operational Value of "warn" Mode: The core value of "warn" mode is "zero-risk deployment of new rules"—when adding a new validation rule, set it to "warn" first, monitor the logs for 1–2 weeks, and track how many existing write operations are rejected. If the violation rate is <1%, the rule is considered safe and can be switched to error mode; if the violation rate is >5%, the rule needs to be adjusted or historical data needs to be cleaned up first. This incremental deployment strategy prevents production incidents such as “widespread errors immediately upon deployment.” To query warn logs, use db.adminCommand({getLog: 'global'}) and filter for the keyword DocumentFailedValidation.
Monitoring Solution for "warn" Logs: "warn" mode logs require proactive monitoring—1. Log filtering: MongoDB "warn" logs are mixed in with other logs; after collection via Filebeat/Fluentd, filter for the keyword "DocumentFailedValidation"; 2. Alert rules: If the number of violations exceeds 10 per hour, trigger a Slack/email alert (indicating the rule may be too strict); 3. Violation Dashboard: Group and count violations by collection, field, and error type to determine which rules need adjustment; 4. Automated Reporting: Generate a daily summary report of violations (Z violations in field Y of collection X) and send it to the DBA and backend teams. The "warn" mode is not a "set-and-forget" approach, but rather a "set-and-monitor-closely" one—only through continuous monitoring can you safely switch from "warn" to "error."
Use Cases:
| Stage | Recommended Action | Reason |
|---|---|---|
| Development/Testing | error | Detect data issues early |
| Initial Phase of New Rule Implementation | warn | Avoid disrupting business operations; monitor violations |
| Once the rules are stable | error | Enforce them to ensure data integrity |
| Data Migration | off | Temporarily disabled to prevent old data from being rejected |
graph LR
A[Write Operation] --> B{Schema Validation}
B -->|Through| C[✅ Write successful]
B -->|Failure + action=error| D[❌ Throw Error, Reject]
B -->|Failure + action=warn| E[⚠️ Write successful + Log Warning]
style C fill:#d4edda
style D fill:#f8d7da
style E fill:#fff3cd
| action | Behavior | Applicable Scenarios |
|---|---|---|
error |
Insert/Update Failed (Exception Thrown) | Production Environment—Data Integrity Takes Priority |
warn |
Allowed but log a warning (do not throw an error) | Phased rollout, observation period |
// === error Pattern(Recommended Production)===
db.createCollection('users', {
validator: { $jsonSchema: {...} },
validationAction: 'error'
});
// === warn Pattern (Lenient) ===
db.createCollection('users', {
validator: { $jsonSchema: {...} },
validationAction: 'warn'
});
// Inserting an Incompatible Document:Success + Warning Log
Key Points Analysis:
- Before switching from "warn" to "error," we recommend first analyzing the frequency of violations in the "warn" logs.
- Logs in warn mode can be viewed via
db.adminCommand({getLog:'global'}) validationAction: 'off'does not exist; to disable validation, setvalidationLevel: 'off'
4. validationLevel
Concept Description: validationLevel determines which documents the validation rules apply to—only new documents (moderate) or including existing documents (strict). This is a core configuration for schema evolution and determines how new rules affect existing data.
How It Works:
strict: All INSERT and UPDATE statements are validated, including when modifying existing documents.moderate: Validation is performed only on newly inserted documents and on updates to documents that already meet the validation rules; updates to historical documents that do not meet the rules are not validated.off: Completely disable validation
Use Cases:
| Scenario | Recommended Level | Reason |
|---|---|---|
| Brand-New Collection | strict | No historical baggage, thoroughly verified |
| New Rules for Existing Sets | moderate | Preventing Old Data from Being Updated |
| Data migration in progress | off | Temporarily disabled; will be re-enabled after migration is complete |
| Consistent Rules + Clean Data | strict | Maximum Protection |
| Level | Behavior | Applicable Scenarios |
|---|---|---|
strict |
Validate all documents (including existing ones) | New collection, clean data |
moderate |
Verify only newly inserted/updated documents (recommended) | Existing collections, phased rollout |
off |
No verification | Data migration |
// === moderate Pattern(Recommendations)===
db.createCollection('users', {
validator: { $jsonSchema: {...} },
validationLevel: 'moderate'
});
// Existing dirty data is not validated,Validate only new data
Key Points Analysis:
- "moderate" is the most commonly used level in production environments; it does not block updates to historical dirty data.
- Before switching from "moderate" to "strict," you must first clean up non-compliant historical data.
collModYou can dynamically modify thevalidationLevelwithout having to rebuild the collection.
Implicit Risks of "moderate": The "validate new data only" approach in "moderate" mode may seem safe, but it carries implicit risks—1. Old data can be updated an unlimited number of times without triggering validation, so dirty data may become increasingly corrupted; 2. Application code may rely on schema rules (such as assuming all documents have an email field), causing the application to crash when old data does not comply; 3. “Moderate” is not a “safe default,” but rather a “migration grace period”—you should switch to “Strict” once migration is complete. Best practice: Start with “Moderate” + “Warn” to monitor the violation rate, and switch to “Strict” + “Error” once the violation rate drops to 0.
Coordination of Data Migration and Validation: Schema validation and data migration must be coordinated—1. When adding a new required field, first set a default value (to prevent old documents from missing required fields), then use a migration script to populate historical data; 2. When adding an enum constraint, first ensure that all historical values fall within the enum range (otherwise, old data can be updated in moderate mode but will result in an error in strict mode); 3. When tightening constraints (e.g., changing maxlength from 200 to 100), first observe the behavior in warn mode to confirm there is no excessively long data before switching to error mode. Migration scripts typically use bulkWrite + $set to batch-update historical data.
5. Modifying the Validator for an Existing Set
Concept Explanation: In a production environment, schemas are not static—business iterations may require adding new fields, modifying rules, or even removing constraints. The collMod command allows you to modify validators for existing collections online, without having to rebuild the collection or take the service offline.
Important Notes on collMod: The collMod validator performs a "full replacement" rather than an "incremental merge"—you must provide the complete $jsonSchema schema with every call, even if you’re changing only one field. This means: 1. You must save the current schema before making changes (use db.getCollectionInfos() to view the existing validator); 2. The new schema must include all fields from the old schema (otherwise, fields not listed will no longer be validated); 3. It is recommended to manage $jsonSchema rules using version control (such as Git) to facilitate rollbacks and audits. To remove a validator, use an empty object { } with validationLevel: 'off', rather than deleting the validator field.
Schema Version Control Practices: $jsonSchema definitions should be included in version control—1. One JSON file per collection (e.g., validators/users.json, validators/orders.json), managed in the same repository as the application code; 2. Deployment scripts should execute collMod in the order of dependencies (users first, then orders, because orders references users._id); 3. Version Numbering: Include the version number and date of changes in a comment at the top of each schema file for easy tracking; 4. Rollback Procedure: Simply revert the schema file using Git and re-run the deployment script to roll back; 5. CI/CD Integration: Automatically execute schema migration scripts within the deployment pipeline to ensure that the code and schema are updated in sync. This set of practices eliminates the classic deployment issue of “code has changed but the database schema hasn’t.”
Rollout Strategy for Schema Migrations: Schema changes should be rolled out in phases, just like code— 1. Phase 1: Make the new field optional (moderate + warn), and monitor for 1–2 weeks to confirm there are no issues; 2. Phase 2: Use migration scripts to backfill historical data (bulkWrite + $set) to ensure all documents contain the new fields; 3. Phase 3: Set the new fields to required (strict + error), enforcing that new documents must include them; 4. Phase 4: Application code begins using the new fields (previously, it was “use if present, ignore if absent”). Each phase is deployed independently; if a problem arises, only the current phase is rolled back. Avoid implementing all changes at once—while “adding fields, populating historical data, and then setting them as required” may seem inefficient, each step is verifiable and rollback-able, making it the only way to ensure production safety.
graph LR
A[Analyzing New Demand] --> B[Design New Rules]
B --> C[validationLevel: moderate<br/>validationAction: warn]
C --> D[Observation Log<br/>Frequency of Violations]
D --> E{Many violations?}
E -->|Many| F[Adjustment Rules/Data Migration]
E -->|Few| G[validationAction: error]
F --> D
G --> H[validationLevel: strict<br/>(Optional)]
style C fill:#fff3cd
style G fill:#d4edda
| Operation | Command | Notes |
|---|---|---|
| Add Validator | collMod + validator |
moderate to avoid affecting old data |
| Modification Rules | collMod + new validator |
Full replacement, not incremental modification |
| Delete Verifier | collMod + validator:{} + level:off |
For temporary deactivation |
| Add a required field | Make new fields optional at first, then required | Take a gradual approach to avoid blocking data entry |
Best Practices for Schema Evolution: Schema changes in production environments require a cautious, incremental approach—1. Adding fields: First set them to optional with a default value (so that legacy data is automatically populated with the default); after running for a while to confirm there are no issues, change them to required; 2. Tightening constraints: First use validationAction: warn to monitor; after confirming there are no violations, switch to error; 3. Removing fields: First, stop writing to the field at the application layer; after confirming that legacy data is no longer being read, use $unset to clean it up in bulk; 4. Renaming fields: First, add the new field and write to both the new and old fields simultaneously; after migrating the data, delete the old field. A rollback plan is required for each step.
The Relationship Between $jsonSchema and JSON Schema: MongoDB’s $jsonSchema is based on the JSON Schema draft-4 specification but has been tailored to include support for bsonType (which extends BSON types such as ObjectId and Decimal128), required, properties, pattern, minimum/maximum, minItems/maxItems, and more. Keywords not supported: $ref (external references are not supported), definitions (reusable definitions are not supported), and anyOf/oneOf/allOf (combined validation is not supported). These limitations mean that $jsonSchema is suitable for “structural validation” (field type + range + format) but not for complex cross-field logical validation—the latter should be implemented at the Mongoose layer.
// === Add a validator to an existing collection ===
db.runCommand({
collMod: 'users',
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['email', 'username'],
properties: {
email: { bsonType: 'string', pattern: '^.+@.+$' }
}
}
},
validationLevel: 'moderate',
validationAction: 'error'
});
// === Modify the Validator ===
db.runCommand({
collMod: 'users',
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['email', 'username', 'age'], // New age Required
properties: {
email: { bsonType: 'string', pattern: '^.+@.+$' },
age: { bsonType: 'int', minimum: 0 }
}
}
}
});
// === Remove Validator ===
db.runCommand({
collMod: 'users',
validator: {},
validationLevel: 'off'
});
Key Points Analysis:
- The validator for
collModperforms a complete replacement, not a merge—you must write the complete rule every time you make a change. - When adding a new "required" field, it is recommended to start with "moderate + warn" and switch to "strict + error" only after confirming that the existing data is acceptable.
- Delete the validator using an empty object
{}; do not delete the "validator" field.
6. mongoose Schema vs MongoDB $jsonSchema
Concept Explanation: Mongoose Schema and MongoDB’s $jsonSchema are two complementary layers of data validation mechanisms. Mongoose performs validation at the application layer (within the Node.js process); it is flexible but applies only to Node.js clients. $jsonSchema performs validation at the database layer (within the mongod process); it is strict but applies to all clients.
Comparative Analysis:
| Dimension | mongoose Schema | MongoDB $jsonSchema |
|---|---|---|
| Execution Layer | Application Layer (Node.js) | Database Layer (MongoDB) |
| Performance | Verified in the application process | Verified in the database |
| Flexibility | ✅ Asynchronous validation, custom functions | ❌ Static rules only |
| Cross-language | ❌ Node.js only | ✅ Works with any driver |
| Complex Validation | ✅ Any JS code | ❌ Limited to JSON Schema |
| Nested Validation | ✅ Deep nesting + references | ✅ Nested properties |
| Custom Error Messages | ✅ Customized by field | ❌ Generic error messages |
| Runtime Modifications | ✅ Dynamic addition/removal | ✅ Online modification of collMod |
graph LR
A[Client Request] --> B[mongoose Schema<br/>Application-Layer Validation]
B -->|Through| C[MongoDB $jsonSchema<br/>Database-Level Validation]
B -->|Failure| D[❌ Application Layer Rejection<br/>Custom Error Messages]
C -->|Through| E[✅ Write successful]
C -->|Failure| F[❌ Database Rejection<br/>DocumentFailedValidation]
style B fill:#cce5ff
style C fill:#d4edda
style D fill:#f8d7da
style F fill:#f8d7da
Best Practices:
- Dual validation at the application layer (Mongoose) and the database layer ($jsonSchema)
- Use Mongoose for fine-grained, dynamic validation (such as "password strength" and "cross-field relationship validation")
- Use
$jsonSchemafor basic schema validation (as a fallback) to prevent direct writes that bypass the application layer
▶ Example 2: Mongoose + $jsonSchema Double Validation
// ShopHub:Two-Factor Verification for User Registration
// 1. mongoose layer: Flexible Validation + Custom Message
const userSchema = new mongoose.Schema({
email: {
type: String,
required: [true, 'Email is required'],
match: [/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, 'Invalid email format']
},
username: {
type: String,
required: true,
minlength: [3, 'Username must be at least 3 characters'],
maxlength: 30,
validate: {
validator: async function(v) {
const count = await this.constructor.countDocuments({ username: v });
return count === 0;
},
message: 'Username already exists'
}
},
age: { type: Number, min: 18, max: 120 },
role: { type: String, enum: ['customer', 'admin', 'moderator'], default: 'customer' }
});
// 2. $jsonSchema layer: Infrastructure Safety Net
db.runCommand({
collMod: 'users',
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['email', 'username'],
properties: {
email: { bsonType: 'string', pattern: '^.+@.+$' },
username: { bsonType: 'string', minLength: 3 },
age: { bsonType: 'int', minimum: 18 },
role: { enum: ['customer', 'admin', 'moderator'] }
}
}
},
validationLevel: 'moderate',
validationAction: 'error'
});
// 3. Effect: Node.js client uses double verification, other clients are caught by $jsonSchema safety net
7. Schema Evolution Strategy
Concept Explanation: Schema evolution is a core operational challenge for MongoDB’s schema-free database. Although MongoDB does not require a predefined schema, production data always has an implicit structure. When business requirements change, validation rules must be modified safely without disrupting business operations.
Principles of Evolution:
- Incremental: optional first, required later; warn first, error later
- Compatibility: The new rules are compatible with existing data and do not retroactively break it.
- Rollback Capability: Each change is logged under the old rules, allowing for a quick rollback if necessary
- Data First: Migrate the data first, then tighten the rules
Checklist for Safe Schema Evolution: Different types of schema changes have varying levels of risk—1. Safe operations (can be executed directly): Adding optional fields, increasing maxLength, decreasing minimum, adding enum values, adding the $jsonSchema attribute (without changing required); 2. Caution Required (Data Migration Required First): Adding a required field, tightening minLength/minimum, removing enum values, changing bsonType; 3. High Risk (Comprehensive Assessment Required): Removing fields, changing field semantics (e.g., changing “age” from “age” to “year of birth”), changing the type of a required field. Safe operations can be performed directly in the production environment; operations requiring caution must first be validated in the staging environment; high-risk operations require a comprehensive migration plan, a rollback strategy, and a phased rollout.
Team Collaboration Guidelines for Schema Evolution: Schema changes involve multiple teams—1. Backend team: Define schema rules + write migration scripts; 2. Frontend team: Adapt forms and displays to accommodate new fields; 3. DBA team: Execute collMod + monitor database performance; 4. QA team: Verify the correctness of migration scripts + develop rollback plans. Collaboration process: 1. Backend submits a PR for schema changes (including migration scripts and rollback scripts); 2. Frontend submits a PR to synchronize and adapt; 3. Code review confirms the scope of the changes; 4. Execute migration and validate in the staging environment; 5. Roll out to production via gradual deployment (start with "warn" errors, then "error" errors); 6. Monitor for 1–2 weeks to confirm no anomalies. This standardized process prevents collaboration issues such as "the schema was changed, but the frontend team wasn’t aware."
Common Evolutionary Patterns:
| Evolution Type | Risk | Strategy |
|---|---|---|
| Add a new optional field | Low | Add a property directly (not required) |
| Add a new required field | Medium | First optional → Data migration → Then required |
| Change Field Type | High | Dual-write Field → Migration → Switch → Delete Old Field |
| Delete Field | Medium | First remove from "required" → Verify there are no dependencies → Delete property |
| Tighten Value Range | Medium | First: moderate + warn → Confirm → error |
(1) Add a New Field (Backward Compatible)
// ✅ Gradual Evolution:The default field is optional.
db.runCommand({
collMod: 'users',
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['email', 'username'], // The old field is still required
properties: {
email: { bsonType: 'string' },
username: { bsonType: 'string' },
age: { bsonType: 'int' } // Add a Field (not required)
}
}
},
validationLevel: 'moderate'
});
(2) Modify the field type (requires data migration)
Field Type Change Process:
graph LR
A[1.Add a New Field<br/>bsonType:New Type] --> B[2.Dual Writing<br/>The application writes to both new and existing fields simultaneously]
B --> C[3.Data Migration<br/>Old Field→New Field]
C --> D[4.Switch Query<br/>Read New Field]
D --> E[5.Delete Old Fields<br/>Confirm that there are no dependencies]
style A fill:#cce5ff
style E fill:#d4edda
// ⚠️ Exercise Caution When Changing Field Types
// 1. Add a dual-write field
db.runCommand({
collMod: 'users',
validator: { /* Add a New Field,Keep the old field */ }
});
// 2. Data Migration Script
db.users.find({ ageStr: { $exists: true } }).forEach(doc => {
db.users.updateOne(
{ _id: doc._id },
{ $set: { age: parseInt(doc.ageStr) }, $unset: { ageStr: '' } }
);
});
// 3. Remove the old field validation
8. Comprehensive Practical Training
Concept Overview: Comprehensive Practical Application integrates all core features of $jsonSchema into a complete product set definition—including type validation, regular expressions, enumerations, ranges, nested documents, array element validation, and more—to demonstrate the full scope of production-grade schema validation.
Production-Level Schema Design Checklist:
| Check Item | Keyword | Is it included? |
|---|---|---|
| Document Type | bsonType: 'object' | ✅ |
| Required Field | required | ✅ |
| String Length | minLength / maxLength | ✅ |
| Regular Expression Pattern | pattern | ✅ |
| Value Range | Minimum / Maximum | ✅ |
| Enumeration value | enum | ✅ |
| Array element | items | ✅ |
| Nested Documents | Nested Properties | ✅ |
| validationLevel | moderate | ✅ |
| validationAction | error | ✅ |
// === Create a Product Collection(Includes complete Schema Validation)===
db.createCollection('products', {
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['sku', 'title', 'price', 'category'],
properties: {
sku: {
bsonType: 'string',
pattern: '^[A-Z0-9-]+$',
maxLength: 50
},
title: {
bsonType: 'string',
minLength: 1,
maxLength: 200
},
price: {
bsonType: 'decimal'
},
category: {
enum: ['Electronics', 'Books', 'Clothing', 'Home']
},
stock: {
bsonType: 'int',
minimum: 0
},
tags: {
bsonType: 'array',
items: { bsonType: 'string' }
}
}
}
},
validationLevel: 'moderate',
validationAction: 'error'
});
▶ Example: Practical Guide to the MongoDB $jsonSchema Validator
// 1. Create a collection with validation rules
db.createCollection('users', {
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['email', 'username', 'age'],
properties: {
email: {
bsonType: 'string',
pattern: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
maxLength: 100
},
username: {
bsonType: 'string',
minLength: 3,
maxLength: 30,
pattern: '^[a-zA-Z0-9_]+$'
},
age: {
bsonType: 'int',
minimum: 18,
maximum: 120
},
role: {
enum: ['customer', 'admin', 'moderator']
}
}
}
},
validationLevel: 'moderate', // Validate only newly inserted records/Update
validationAction: 'error' // Reject Illegal Data
});
// 2. Testing Valid Data → Success
db.users.insertOne({
email: 'alice@example.com',
username: 'alice_2026',
age: 28,
role: 'customer'
});
// { acknowledged: true, insertedId: ObjectId('...') }
// 3. Testing Invalid Data → Rejected
db.users.insertOne({
email: 'invalid-email', // Invalid email address
username: 'ab', // The username is too short
age: 15 // Under 18 years old
});
// Throw an error:Document failed validation
// The error message includes the field name and the reason for the failure.
// 4. Modify the Validator(Add a New Rule)
db.runCommand({
collMod: 'users',
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['email', 'username', 'age', 'phone'],
properties: {
email: { bsonType: 'string', pattern: '^.+@.+$' },
username: { bsonType: 'string', minLength: 3 },
age: { bsonType: 'int', minimum: 18 },
phone: { bsonType: 'string', pattern: '^\+?[0-9]{10,15}$' } // New
}
}
}
});
// 5. Nested Document Validation
db.createCollection('orders', {
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['userId', 'items', 'total'],
properties: {
userId: { bsonType: 'objectId' },
items: {
bsonType: 'array',
minItems: 1,
items: {
bsonType: 'object',
required: ['productId', 'qty', 'price'],
properties: {
productId: { bsonType: 'objectId' },
qty: { bsonType: 'int', minimum: 1 },
price: { bsonType: 'decimal', minimum: 0 }
}
}
},
total: { bsonType: 'decimal', minimum: 0 }
}
}
}
});
// 6. Turn Off the Verifier(For example, when migrating data)
db.runCommand({
collMod: 'users',
validator: {},
validationLevel: 'off'
});
Output: Valid data is successfully inserted; invalid data is rejected, and the specific fields that failed are reported. validationLevel: moderate Ensures that existing data remains unaffected.
Operational Strategy for Schema Validation: Schema Validation requires operational support in production environments—1. Deployment Strategy: First, set validationAction: 'warn' (log only, do not reject), monitor for 1–2 weeks to confirm that validations are working correctly, then switch to error; 2. Emergency Rollback: Prepare a rollback command (db.runCommand({collMod: 'users', validator: {}, validationLevel: 'off'}) to quickly disable validation in case of false positives; 3. Data Migration: Disable validation (validationLevel: 'off') before running migration scripts, then re-enable it after migration; 4. Monitoring and Alerts: Monitor “validation failed” events in MongoDB logs; frequent failures indicate that validation rules need adjustment; 5. Version Control: Store the $jsonSchema definition in version control (JSON file + deployment script) and release it in sync with the application code.
Safe Path for Evolving Validation Rules: Modifications to schema validation rules fall into three categories—1. Relaxing rules (safe): Adding optional fields, increasing maxLength, or lowering minimum without breaking existing data; 2. Tightening rules (risky): Adding required fields, narrowing the enum range, or raising minimum—existing data may not meet the new rules; 3. Type changes (most dangerous): Changing the bsonType from string to int, etc., which will almost certainly cause validation failures. Safe evolution path: First, relax the rules (make new fields optional) → Complete the data (use scripts to populate new fields in existing documents) → Then tighten the rules (set new fields as required). Allow 1–2 weeks between each step to ensure data consistency.
❓ FAQ
📖 Summary
- $jsonSchema validator: Defines the document structure
- validationAction: error (reject) / warn (warning)
- validationLevel: strict (all) / moderate (new data only; recommended)
- mongoose Schema vs $jsonSchema: complementary
- Schema Evolution: Add Features Gradually, Modify Types Carefully
📝 Exercises
- Basic Question (⭐): Create a $jsonSchema validation for the
userscollection (email/username/age). - Basic Question (⭐): Tests the difference in behavior between
validationAction: warnandvalidationAction: error. - Advanced Exercise (⭐⭐): Use
collModto modify the validator of an existing collection (add a new field). - Advanced Problem (⭐⭐): Implement dual validation using Mongoose and $jsonSchema.
- Challenge Question (⭐⭐⭐): The complete $jsonSchema definition for the product collection (including nested elements, arrays, and Decimal128).



