MongoDBMongoDB Data Types

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

JS
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

Warning
Never store money as a 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

JS
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

JS
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

JS
ObjectId()                          // generates a new one
ObjectId("64f1a2b3c4d5e6f7a8b9c0d1")  // parses an existing hex string
ObjectId().getTimestamp()           // Date the id was generated
Arrays and Embedded Documents

Arrays and nested documents

JS
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

JS
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" } })
Note
BSON has no separate 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

JS
db.files.insertOne({
  name: "avatar.png",
  data: BinData(0, "iVBORw0KGgoAAAANSUhEUgAA...")   // subtype 0 = generic binary
})
Note
For files larger than 16 MB (or that you simply don't want bloating a document), use GridFS instead of an inline Binary field.
Checking Types with $type

Auditing field types across a collection

JS
// 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 mongosh shell (and the Node.js driver by default) represents whole numbers as JavaScript numbers, which lose precision above 2^53 — wrap large integers explicitly with NumberLong().

  • Comparing a Double field to an Int32 literal 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 Decimal128 of 10 sorts identically to an Int32 of 10.

  • Use $type: "number" (the umbrella alias) rather than a specific numeric subtype when you just want "is this a number, whatever the exact representation."

Tip
When in doubt about which numeric type a driver sent, run a 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.