Every field in a MongoDB document has a BSON data type. Getting types right matters because type mismatches cause silent query failures, sort order surprises, and validation errors that are difficult to debug. When you use Mongoose, the schema you define maps JavaScript types to BSON types automatically — but you need to understand the underlying BSON type system to design schemas correctly, interpret Compass and mongosh output, and troubleshoot the type-related bugs that every MongoDB developer encounters sooner or later.
BSON Types and Their JavaScript Equivalents
| BSON Type | Mongoose Schema Type | JavaScript Type | Common Use |
|---|---|---|---|
| String | String |
string |
Text fields — title, body, slug, email |
| Int32 / Int64 | Number |
number |
Integer counts — viewCount, pageNumber, quantity |
| Double | Number |
number |
Decimal numbers — price, rating, latitude |
| Boolean | Boolean |
boolean |
Flags — published, active, verified, featured |
| Array | [SchemaType] |
Array |
Multi-value fields — tags, roles, images |
| Object (sub-document) | Nested schema or Object |
object |
Embedded data — author, address, metadata |
| ObjectId | mongoose.Schema.Types.ObjectId |
string (24 hex) |
Document IDs, foreign references |
| Date | Date |
Date |
Timestamps — createdAt, updatedAt, publishedAt |
| Null | null default |
null |
Explicitly absent optional fields |
| Binary | Buffer |
Buffer |
Raw binary data — file content, hashed tokens |
| Decimal128 | mongoose.Schema.Types.Decimal128 |
string |
High-precision decimals — financial amounts |
| Mixed | mongoose.Schema.Types.Mixed |
any | Unstructured data — use sparingly |
number), but BSON distinguishes between Int32, Int64, and Double. Mongoose maps all JavaScript number values to Double by default. This is fine for most use cases, but if you need strict integer storage (e.g. for financial calculations) use mongoose.Schema.Types.Decimal128 instead of Number.Date type for all timestamp fields rather than storing timestamps as strings or Unix epoch numbers. MongoDB’s Date type sorts correctly, supports date comparison operators ($gt, $lt, $gte, $lte), and is automatically handled by Mongoose’s timestamps: true option. Storing dates as strings is a common source of sorting bugs.Mixed type (mongoose.Schema.Types.Mixed) tells Mongoose that a field can contain any type. This effectively disables schema validation for that field. Avoid it except for truly unstructured data you cannot model in advance — like a JSON metadata blob from an external API. For every field you can predict, use an explicit type for better validation and query performance.String Type
// Mongoose schema — String examples
const postSchema = new mongoose.Schema({
title: {
type: String, // BSON: String
required: true,
trim: true, // auto-removes leading/trailing whitespace
minlength: 3,
maxlength: 200,
},
slug: {
type: String,
lowercase: true, // auto-converts to lowercase before saving
unique: true,
},
status: {
type: String,
enum: ['draft', 'published', 'archived'], // only these values allowed
default: 'draft',
},
});
// mongosh — string queries
db.posts.find({ status: 'published' }) // exact match
db.posts.find({ title: /mern/i }) // regex — case-insensitive contains 'mern'
db.posts.find({ title: { $regex: '^Getting' } }) // starts with 'Getting'
Number Type
// Mongoose schema — Number examples
const postSchema = new mongoose.Schema({
viewCount: { type: Number, default: 0, min: 0 },
rating: { type: Number, min: 0, max: 5 },
partNum: { type: Number, min: 1, validate: {
validator: Number.isInteger,
message: 'Part number must be an integer',
}},
});
// mongosh — number queries
db.posts.find({ viewCount: { $gt: 100 } }) // greater than 100
db.posts.find({ viewCount: { $gte: 100 } }) // greater than or equal
db.posts.find({ viewCount: { $lt: 50 } }) // less than 50
db.posts.find({ viewCount: { $between: [10, 100] } }) // between (use $gte + $lte)
db.posts.find({ viewCount: { $gte: 10, $lte: 100 } }) // between 10 and 100
// Incrementing a number
db.posts.updateOne({ _id: id }, { $inc: { viewCount: 1 } }) // +1
db.posts.updateOne({ _id: id }, { $inc: { viewCount: -1 } }) // -1
Date Type
// Mongoose — Date examples
const postSchema = new mongoose.Schema({
publishedAt: { type: Date, default: null },
scheduledFor: { type: Date },
}, { timestamps: true }); // auto adds createdAt and updatedAt as Date fields
// Saving dates
const post = await Post.create({
title: 'My Post',
publishedAt: new Date(), // current time
scheduledFor: new Date('2025-12-01'), // specific date
});
// mongosh — date range queries
const today = new Date();
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
db.posts.find({ createdAt: { $gte: yesterday, $lte: today } }) // last 24 hours
db.posts.find({ publishedAt: { $lt: new Date() } }) // already published
Array Type
// Mongoose — Array examples
const postSchema = new mongoose.Schema({
tags: { type: [String], default: [] },
images: { type: [String] },
// Array of ObjectIds (references)
likedBy: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
});
// mongosh — array queries
db.posts.find({ tags: 'mern' }) // documents where tags array contains 'mern'
db.posts.find({ tags: { $all: ['mern', 'javascript'] } }) // contains ALL of these
db.posts.find({ tags: { $in: ['mern', 'react'] } }) // contains ANY of these
db.posts.find({ tags: { $size: 3 } }) // tags array has exactly 3 elements
// Array update operators
db.posts.updateOne({ _id: id }, { $push: { tags: 'new-tag' } }) // add element
db.posts.updateOne({ _id: id }, { $addToSet: { tags: 'unique-tag' } }) // add if not exists
db.posts.updateOne({ _id: id }, { $pull: { tags: 'old-tag' } }) // remove element
ObjectId Type
// Mongoose — ObjectId reference
const postSchema = new mongoose.Schema({
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User', // Mongoose uses this for populate()
required: true,
},
});
// Creating an ObjectId from a string
const { Types } = require('mongoose');
const id = new Types.ObjectId('64a1f2b3c8e4d5f6a7b8c9d0');
// Validating ObjectId format
Types.ObjectId.isValid('64a1f2b3c8e4d5f6a7b8c9d0'); // true
Types.ObjectId.isValid('not-an-id'); // false
Types.ObjectId.isValid('12345'); // false
// Extracting the creation time from an ObjectId
const oid = new Types.ObjectId('64a1f2b3c8e4d5f6a7b8c9d0');
const createdAt = oid.getTimestamp(); // returns a Date object
Common Mistakes
Mistake 1 — Storing dates as strings
❌ Wrong — storing a date as a formatted string:
publishedAt: { type: String } // "2025-01-15T10:30:00.000Z"
// String comparison ≠ date comparison
// db.posts.find({ publishedAt: { $gt: "2025-01-01" } }) — unreliable sort
✅ Correct — always use the Date type for timestamps:
publishedAt: { type: Date } // Date comparison operators work correctly ✓
Mistake 2 — Using Mixed type for fields you can model explicitly
❌ Wrong — using Mixed for a settings object with known fields:
settings: { type: mongoose.Schema.Types.Mixed } // no validation, no structure
✅ Correct — model sub-documents explicitly:
settings: {
emailNotifications: { type: Boolean, default: true },
theme: { type: String, default: 'light', enum: ['light', 'dark'] },
}
Mistake 3 — Querying a Number field with a String value
❌ Wrong — passing a string where a number is expected in a query:
await Post.find({ viewCount: '100' }); // '100' (string) never matches viewCount: 100 (number)
✅ Correct — ensure query values match the field’s type:
await Post.find({ viewCount: 100 }); // number literal ✓
await Post.find({ viewCount: parseInt('100', 10) }); // parse from string if needed ✓
Quick Reference
| Use Case | Mongoose Schema Type |
|---|---|
| Text field | String |
| Integer or decimal | Number |
| True/false flag | Boolean |
| Timestamp | Date |
| Document ID / reference | mongoose.Schema.Types.ObjectId |
| List of values | [String] or [ObjectId] |
| Nested object | Nested schema definition |
| Allowed values only | enum: ['a', 'b', 'c'] |
| Auto timestamps | { timestamps: true } in schema options |