MongoDB BSON Data Types — A Complete Guide

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
Note: JavaScript has only one numeric type (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.
Tip: Use the 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.
Warning: The 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

🧠 Test Yourself

You want to query all posts published after January 1, 2025. Your publishedAt field is stored as a String type in the format “YYYY-MM-DDTHH:mm:ss.sssZ”. What problem will you encounter?