Data Types
MongoDB documents can hold any BSON type in any field, and the same field name can even hold different types across different documents in a collection. Knowing the full type palette — and its sharp edges — is essential for correct queries, correct sorting, and correct math.
Strings
Strings are UTF-8
db.users.insertOne({ name: "José García", bio: "Café ☕ enthusiast" })
// String comparisons are byte-order by default — not locale-aware.
// Use collation for locale-correct sorting/comparison (see the Sorting page).
db.users.find({ name: { $regex: /^José/ } })Numbers — Four Different Types
Unlike JavaScript's single number type, BSON distinguishes four numeric types. mongosh defaults to Double for any number literal you type — you must explicitly wrap a value to get Int32, Int64, or Decimal128.
Type | mongosh Constructor | Range / Precision | Typical Use |
|---|---|---|---|
Double | 3.14 (bare literal) | ~15-17 significant decimal digits (IEEE 754) | General-purpose decimals — default type |
Int32 | NumberInt(42) | -2,147,483,648 to 2,147,483,647 | Counters, small whole numbers |
Int64 | NumberLong(42) | ±9.2 quintillion | Large counters, IDs that exceed 32-bit range |
Decimal128 | NumberDecimal("9.99") | 34 significant decimal digits, exact base-10 | Money and anything requiring exact decimal math |
Double. IEEE 754 floating point cannot represent most decimal fractions exactly — 0.1 + 0.2 famously does not equal 0.3. Repeated arithmetic on doubles compounds rounding error. Always use Decimal128 (NumberDecimal("...")) for currency, or store amounts as integer cents.Decimal128 for exact money math
db.invoices.insertOne({
subtotal: NumberDecimal("19.99"),
tax: NumberDecimal("2.60"),
total: NumberDecimal("22.59")
})
// Aggregation preserves Decimal128 precision through $sum
db.invoices.aggregate([
{ $group: { _id: null, grandTotal: { $sum: "$total" } } }
])Dates
BSON Date stores milliseconds since the Unix epoch (UTC), regardless of what time zone was used to construct it. Always create dates with new Date() or ISODate() — never as plain strings.
Working with dates
db.events.insertOne({
name: "Launch",
startsAt: new Date("2024-06-01T09:00:00Z"),
createdAt: new Date()
})
// Range queries work naturally because Date compares chronologically
db.events.find({
startsAt: { $gte: ISODate("2024-01-01"), $lt: ISODate("2025-01-01") }
})ObjectId
The default type for _id. A 12-byte value that is globally unique without coordination and roughly sortable by creation time. See the dedicated ObjectId page for its internal anatomy.
ObjectId basics
ObjectId() // generates a new one
ObjectId("64f1a2b3c4d5e6f7a8b9c0d1") // parses an existing hex string
ObjectId().getTimestamp() // Date the id was generatedArrays and Embedded Documents
Arrays and nested documents
db.products.insertOne({
name: "Widget",
tags: ["new", "sale", "featured"], // array of strings
variants: [ // array of embedded documents
{ color: "red", stock: 10 },
{ color: "blue", stock: 5 }
],
dimensions: { w: 10, h: 5, d: 2 } // a single embedded document
})Null vs Missing (Undefined)
BSON distinguishes an explicit null value from a field that is simply absent. Queries treat them almost identically by default, which trips up a lot of newcomers.
Null vs missing field
db.users.insertMany([
{ name: "A", middleName: null }, // field present, value is null
{ name: "B" } // field absent entirely
])
// { middleName: null } matches BOTH documents by default!
db.users.find({ middleName: null })
// To match ONLY documents where the field truly does not exist:
db.users.find({ middleName: { $exists: false } })
// To match ONLY documents where the field exists AND is null:
db.users.find({ middleName: { $type: "null" } })undefined type in modern usage — it exists in the spec but is deprecated. Drivers typically serialize a JavaScript undefined field value as BSON null or omit the field; don't rely on distinguishing the two.Binary Data
Storing binary data
db.files.insertOne({
name: "avatar.png",
data: BinData(0, "iVBORw0KGgoAAAANSUhEUgAA...") // subtype 0 = generic binary
})Binary field.Checking Types with $type
Auditing field types across a collection
// Find documents where "price" is not stored as a number
db.products.find({ price: { $not: { $type: "number" } } })
// $type accepts an array to match several types at once
db.products.find({ sku: { $type: ["string", "int"] } })> db.products.aggregate([
... { $group: { _id: { $type: "$price" }, count: { $sum: 1 } } }
... ])
[
{ _id: 'double', count: 240 },
{ _id: 'string', count: 3 },
{ _id: 'decimal', count: 5 }
]JavaScript Number Precision Pitfalls
The
mongoshshell (and the Node.js driver by default) represents whole numbers as JavaScript numbers, which lose precision above 2^53 — wrap large integers explicitly withNumberLong().Comparing a
Doublefield to anInt32literal in a query still matches correctly — MongoDB compares by numeric value across number subtypes, not by exact BSON type.Sorting mixes all number types together by value; a
Decimal128of10sorts identically to anInt32of10.Use
$type: "number"(the umbrella alias) rather than a specific numeric subtype when you just want "is this a number, whatever the exact representation."
findOne() and inspect the value in mongosh — it prints NumberLong(...) / NumberDecimal(...) wrappers explicitly for non-double types, but prints bare numbers for plain Double values.