MongoDB Data Types: A Practical Guide to BSON
MongoDB does not store JSON. It stores BSON, a binary serialization format that looks like JSON when you print it but carries a richer set of types underneath. That distinction matters in practice: a value that prints as 42 might be a 32-bit integer, a 64-bit integer, or a double, and which one it is changes how comparisons, indexes, and arithmetic behave.
This guide covers the BSON types you will actually encounter, the ones that cause subtle bugs, and how to inspect and query by type.
BSON vs JSON
JSON has six types: string, number, boolean, null, object, and array. BSON keeps all of those and adds typed extensions that JSON cannot represent, including a dedicated date type, three distinct numeric types, binary data, and ObjectId. BSON is also length-prefixed, which is what makes MongoDB able to skip over fields without parsing them and to traverse documents quickly.
When you export BSON to JSON, the extra types have to be encoded somehow. MongoDB uses a convention called Extended JSON, where a date becomes {"$date": "..."} and a 64-bit integer becomes {"$numberLong": "..."}. If you have ever seen those $-prefixed wrappers in a mongoexport dump, that is BSON type information surviving the round trip to text.
The full type table
Each BSON type has a numeric and a string identifier. The string alias is what you use with the $type operator.
| Type | Alias | Notes |
|---|---|---|
| Double | "double" | 64-bit IEEE 754 float |
| String | "string" | UTF-8 |
| Object | "object" | Embedded document |
| Array | "array" | |
| Binary data | "binData" | Has subtypes (UUID, MD5, etc.) |
| ObjectId | "objectId" | 12 bytes |
| Boolean | "bool" | |
| Date | "date" | 64-bit ms since Unix epoch |
| Null | "null" | |
| Regular Expression | "regex" | |
| 32-bit integer | "int" | |
| Timestamp | "timestamp" | Internal replication type, not a wall-clock date |
| 64-bit integer | "long" | |
| Decimal128 | "decimal" | 128-bit exact decimal |
| Min key | "minKey" | Sorts before all other values |
| Max key | "maxKey" | Sorts after all other values |
A handful of older types (undefined, dbPointer, symbol, and JavaScript-with-scope) are deprecated and should not appear in new data.
ObjectId
ObjectId is the default type for the _id field if you do not supply one. It is 12 bytes:
- A 4-byte timestamp (seconds since the Unix epoch) marking creation.
- A 5-byte random value, unique per process.
- A 3-byte counter, initialized randomly per process.
Two consequences follow from that layout. First, because the leading bytes are a timestamp, ObjectId values sort in roughly creation order, so sorting by _id is close to sorting by insertion time without an extra field. Second, you can extract the creation time directly:
ObjectId("507f1f77bcf86cd799439011").getTimestamp()
// ISODate("2012-10-17T20:46:15.000Z")The timestamp has one-second resolution, so it is fine for ordering but not for anything that needs millisecond precision. If you need exact creation time, store an explicit Date.
Date vs Timestamp
This is the most common type confusion in MongoDB, because the names suggest they are interchangeable. They are not.
Date is what you want for application data: it is a signed 64-bit integer counting milliseconds since the Unix epoch, and it represents an actual point in time. Create one with new Date():
db.events.insertOne({ name: "signup", at: new Date() })Timestamp is an internal type used by MongoDB for replication ordering in the oplog. It is a 64-bit value split into a seconds field and an incrementing ordinal. You will see it in system collections, but you almost never want it for your own fields. If you store Timestamp() where you meant a date, sorting and date arithmetic will behave in ways you do not expect.
A subtle point: BSON Date can hold negative values, representing dates before 1970. Some language drivers historically struggled with pre-epoch dates, so verify behavior if you store historical data.
The three number types
JSON has one number type. BSON has three, and the difference is a frequent source of bugs.
- Double (
"double") is a 64-bit floating-point number. Inmongosh, any bare number literal like42or3.14becomes a double by default. - 32-bit integer (
"int") holds whole numbers up to about 2.1 billion. Created withNumberInt("42"). - 64-bit integer (
"long") holds whole numbers up to about 9.2 quintillion. Created withNumberLong("42").
The trap: because the shell defaults numeric literals to double, a field you think is an integer is often a double. Doubles cannot represent every large integer exactly, so a counter stored as a double can lose precision past 2^53. If you are storing IDs, counters, or any large whole number, use NumberLong explicitly:
db.counters.insertOne({ _id: "views", n: NumberLong("9007199254740993") })Mixed numeric types in the same field also affect comparison and sorting. MongoDB will compare across numeric types, but querying { n: 42 } (a double) against data stored as NumberInt(42) works because the values compare equal, while exact-type matching with $type does not. Be deliberate about which type you write.
Decimal128 for money
Floating-point doubles cannot represent values like 0.1 exactly, which is why 0.1 + 0.2 does not equal 0.3 in binary floating point. For currency, tax, or any value where rounding errors are unacceptable, use Decimal128, a 128-bit decimal type with 34 digits of precision and exact decimal rounding:
db.invoices.insertOne({
total: NumberDecimal("19.99"),
tax: NumberDecimal("1.60")
})Quote the value as a string inside NumberDecimal. If you write NumberDecimal(19.99) with a bare number, the value is first parsed as an imprecise double and then converted, defeating the purpose. Always pass a string.
The common alternative is to store money as an integer number of cents using NumberLong and divide for display. That avoids decimals entirely and is a reasonable choice; Decimal128 is the better fit when you need fractional units beyond cents or want the stored value to read naturally.
Binary data and UUIDs
The binData type stores raw bytes and carries a subtype byte indicating interpretation: generic binary, UUID, MD5, encrypted values, and (since MongoDB 5.2) compressed time-series and vector data. If you store UUIDs, use the UUID subtype rather than a hex string so comparisons and storage stay compact. Be aware that historically different drivers encoded UUIDs with different byte orders, so cross-language UUID handling needs care.
Querying by type with $type
Because a single field can hold values of different BSON types across documents, MongoDB lets you query by type. This is the practical tool for finding the data inconsistencies described above.
// Find documents where "n" was accidentally stored as a double
db.counters.find({ n: { $type: "double" } })
// Find documents where "total" is any numeric type
db.invoices.find({ total: { $type: "number" } })The "number" alias matches int, long, double, and decimal together, which is the quick way to find every numeric document regardless of its specific type. In the aggregation pipeline, the $type expression returns a field's type as a string and $isNumber returns a boolean, both useful for cleaning up mixed data:
db.counters.aggregate([
{ $project: { n: 1, t: { $type: "$n" } } }
])A type-audit query
When you inherit a collection and want to know what is actually stored in a field, group by type:
db.events.aggregate([
{ $group: { _id: { $type: "$at" }, count: { $sum: 1 } } }
])If that returns more than one row for a field you expected to be uniform, you have mixed types to clean up before they cause query or index surprises.
Inspecting types with Mako
Type mismatches are invisible until a query silently returns nothing or a sort comes out in the wrong order. Mako connects to MongoDB and offers AI-assisted query writing, which helps when you are auditing a field's actual types with $type or comparing how Date, Decimal128, and the numeric types behave under your real queries. Mako is a read and query tool, so it suits inspecting and analyzing data rather than administering the deployment.
To load sample data and experiment, see our guides on importing CSV, JSON, and Excel into MongoDB. For filtering by type in practice, see the query operators guide, and for how types affect index behavior, the indexes guide.
Mako connects to MongoDB, PostgreSQL, MySQL, and more with AI-powered autocomplete. Try it free at mako.ai.
Skip the terminal. Use Mako.
Connect your database, write queries with AI assistance, and import/export data in clicks. Free to start.