Free & open source — no account required
Free & open source — no account required
This technical guide provides an in-depth analysis of the json to mongodb schema engine, best practices for implementation, and data security standards.
MongoDB is schemaless by default — any document can go into any collection. But "schemaless" doesn't mean "no thinking required." Professional MongoDB deployments use $jsonSchema validators enforced at the database level, carefully consider the embed-vs-reference decision based on access patterns, design aggregation pipelines around the document shape, and build Atlas Search indexes for full-text capabilities. Converting your JSON to a MongoDB schema is designing those data access patterns, not just mapping types.
// Input JSON
{
"customerId": "CUST-99",
"name": "Tech Corp",
"contacts": [
{ "name": "Alice Chen", "email": "alice@tech.com", "isPrimary": true },
{ "name": "Bob Smith", "email": "bob@tech.com", "isPrimary": false }
],
"tier": "enterprise",
"metadata": {
"industry": "software",
"employeeCount": 250
},
"isActive": true
}
// Generated MongoDB $jsonSchema Validator
db.createCollection("customers", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["customerId", "name", "tier", "isActive"],
additionalProperties: false,
properties: {
_id: { bsonType: "objectId" },
customerId: {
bsonType: "string",
pattern: "^CUST-\\d+$",
description: "must match CUST-NNN format"
},
name: { bsonType: "string", minLength: 1, maxLength: 200 },
contacts: {
bsonType: "array",
maxItems: 50,
items: {
bsonType: "object",
required: ["name", "email", "isPrimary"],
properties: {
name: { bsonType: "string" },
email: { bsonType: "string", pattern: "^[^@]+@[^@]+\\.[^@]+$" },
isPrimary: { bsonType: "bool" }
}
}
},
tier: {
bsonType: "string",
enum: ["starter", "professional", "enterprise"]
},
metadata: { bsonType: "object" },
isActive: { bsonType: "bool" }
}
}
},
validationAction: "error", // reject invalid documents
validationLevel: "strict" // apply to inserts AND updates
});
// Indexes
db.customers.createIndex({ customerId: 1 }, { unique: true });
db.customers.createIndex({ tier: 1, isActive: 1 });
db.customers.createIndex({ "contacts.email": 1 });
validationAction: "error" rejects writes that violate the schema — the alternative "warn" logs violations without rejecting. Use "error" for production data integrity. additionalProperties: false prevents arbitrary fields from being added, which is useful for collections where you need to control the schema strictly.
// JSON has 6 types: string, number, boolean, null, array, object
// BSON adds:
// ObjectId — 12-byte document identifier with embedded timestamp
// Date — 64-bit Unix timestamp (milliseconds)
// Decimal128 — 128-bit decimal for financial precision
// Binary — raw bytes
// Regex — compiled regular expression
// Using BSON types in Node.js (mongodb driver)
const { ObjectId, Decimal128 } = require('mongodb');
// Insert with explicit BSON types
await db.collection('orders').insertOne({
_id: new ObjectId(), // auto-generated if omitted
customerId: new ObjectId("507f1f77bcf86cd799439011"),
amount: Decimal128.fromString("149.99"), // preserve exact cents
createdAt: new Date(), // BSON Date (not ISO string!)
tags: ["urgent", "enterprise"],
});
// $jsonSchema for BSON types
{
bsonType: "object",
properties: {
_id: { bsonType: "objectId" },
amount: { bsonType: "decimal" }, // Decimal128
createdAt: { bsonType: "date" }, // BSON Date
data: { bsonType: "binData" } // Binary
}
}
The single most important MongoDB schema decision: should related data be embedded or referenced?
// EMBED when data is:
// - Always read together with the parent
// - Not queried independently across many parents
// - Bounded in size (won't grow unboundedly)
// Example: user.address (always fetched with user, stable size)
// Embedded design
{
"_id": "usr_001",
"name": "Alice",
"address": { // embedded — always fetched with user
"street": "123 Main St",
"city": "Portland",
"zip": "97201"
}
}
// REFERENCE when data is:
// - Queried independently
// - Shared by multiple documents
// - Large or unboundedly growing
// Example: comments on a post (can grow to thousands)
// Referenced design
// posts collection:
{ "_id": ObjectId("..."), "title": "My Post", "authorId": ObjectId("...") }
// comments collection (referenced, not embedded):
{ "_id": ObjectId("..."), "postId": ObjectId("..."), "body": "Great post!", "createdAt": Date }
// Avoid embedding when arrays can grow without bound —
// each document has a 16MB BSON limit
// A post with 100k embedded comments hits that limit
Your document shape determines which aggregation stages you'll use most. Plan for the aggregations you'll need before finalizing the schema:
const pipeline = [
// Stage 1: Filter early to reduce work
{
$match: {
isActive: true,
tier: { $in: ["professional", "enterprise"] },
}
},
// Stage 2: Unwind embedded arrays for per-element analysis
{ $unwind: "$contacts" },
// Stage 3: Filter on unwound elements
{ $match: { "contacts.isPrimary": true } },
// Stage 4: Lookup (JOIN) to another collection
{
$lookup: {
from: "orders",
localField: "_id",
foreignField: "customerId",
as: "orders",
pipeline: [
{ $match: { status: "completed" } },
{ $project: { amount: 1, completedAt: 1 } },
]
}
},
// Stage 5: Group and aggregate
{
$group: {
_id: "$tier",
customerCount: { $sum: 1 },
totalRevenue: { $sum: { $reduce: {
input: "$orders",
initialValue: 0,
in: { $add: ["$$value", "$$this.amount"] }
}}},
primaryContacts: { $push: "$contacts.email" }
}
},
// Stage 6: Sort results
{ $sort: { totalRevenue: -1 } }
];
const results = await db.collection("customers").aggregate(pipeline).toArray();
// Define an Atlas Search index (Atlas-hosted MongoDB only)
{
"mappings": {
"dynamic": false,
"fields": {
"name": {
"type": "string",
"analyzer": "lucene.standard"
},
"metadata.industry": {
"type": "string",
"analyzer": "lucene.keyword"
},
"tier": {
"type": "token" // exact match only
}
}
}
}
// Full-text search aggregation
const searchResults = await db.collection("customers").aggregate([
{
$search: {
index: "customers_search",
compound: {
must: [
{
text: {
query: "technology startup",
path: "name",
fuzzy: { maxEdits: 1 }
}
}
],
filter: [
{ equals: { path: "isActive", value: true } }
]
}
}
},
{
$project: {
name: 1, tier: 1,
score: { $meta: "searchScore" }
}
},
{ $sort: { score: -1 } },
{ $limit: 10 }
]).toArray();
// Watch for all changes to a collection
const changeStream = db.collection("customers").watch([
{
$match: {
operationType: { $in: ["insert", "update", "delete"] },
"fullDocument.tier": "enterprise"
}
}
], { fullDocument: "updateLookup" });
changeStream.on("change", async (change) => {
if (change.operationType === "insert") {
await notifyAccountTeam(change.fullDocument);
}
if (change.operationType === "update") {
await syncToDataWarehouse(change.fullDocument);
}
});
// Change streams require a replica set (or Atlas)
// They resume automatically after network interruptions using a resumeToken
"CUST-99" work but require a separate field for creation time and are less compact in indexes.additionalProperties: false to strict collections: For collections where the schema should be controlled, this prevents accidental field additions from application bugs.$lookup scans the joined collection without an index. Always add an index on the foreignField used in a lookup to prevent collection scans on joins.Q: When should I use $jsonSchema validation vs. application-layer validation (Mongoose/Zod)?
A: Both. $jsonSchema at the database level is the last line of defense — it catches writes from any client, not just your application. Application-layer validation (Mongoose, Zod) gives better error messages, runs before the network call, and can validate cross-field logic. Use both: validate in the app first for UX, rely on DB validation as a safety net.
Q: How do I handle multi-document transactions?
A: MongoDB supports ACID transactions across multiple documents and collections (requires replica set or Atlas). Use them for operations that must be atomic across collections — but prefer embedding related data to avoid needing transactions in the first place. Transactions have a 60-second timeout and impact cluster performance.
Q: How do I model a many-to-many relationship?
A: Two options: (1) Store an array of ObjectIds on both sides, keep the array bounded. (2) Use a junction collection (like SQL) for relationships that need metadata (e.g., user-role assignments with timestamps). The junction approach handles large cardinalities better.
Q: What is the BSON 16MB document size limit in practice?
A: For typical business data (user profiles, orders, products), it is virtually impossible to hit. It becomes a concern only with embedded arrays that grow without bound — thousands of embedded comments, event logs, or history records. If an array might exceed hundreds of items, move it to a separate collection.
Is the processing local-only?
Absolutely. TypeMorph operates entirely within your browser's sandbox. We use Web Workers for high-performance computation without ever transmitting your JSON, SQL, or API data to a remote server.
Can I use this for enterprise projects?
Yes. The tool is designed for professional software engineers who require GDPR compliance and data privacy. It is trusted by developers at top-tier startups and financial institutions.
Why pasting proprietary company data into third-party web tools is a major liability, and how to stay safe.
Code generation is just the beginning. Discover how a schema-first approach can eliminate 90% of your integration bugs.