Documents, BSON & data types

This section documents the current MongoDB 8.x server line as published at the MongoDB Server Manual, which is the reference these pages are written and verified against. No specific patch version is pinned. Some capabilities (Atlas Search, Atlas Vector Search, and parts of encryption and backup) are Atlas-only — they are linked, not documented in depth.

This content was generated with the assistance of AI and should be verified against the official manual before being relied on in production, since MongoDB iterates quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

A MongoDB record is a document: an ordered set of field/value pairs, stored on disk and on the wire in a binary format called BSON. This page covers what a document may contain, the type system BSON provides, how _id and ObjectId work, how to reach into nested structure, and how values of different types compare.

The document and the BSON format

A document is an ordered list of fields; each field has a name (a string) and a value (one of the BSON types below). Field order is preserved as written and is significant for equality of embedded documents.

{
  "_id": 1,
  "title": "BSON basics",
  "tags": ["bson", "types"],
  "meta": { "views": 5, "draft": false }
}

BSON ("Binary JSON") is a length-prefixed binary encoding. It exists to add what plain JSON lacks for a database: distinct numeric types (32-bit vs 64-bit integers vs Decimal128), a native date type, a binary type, and an ObjectId type, plus fast traversal because every value is length-prefixed. It also drops JSON’s whitespace and repeats field names as raw bytes. See Documents in the manual for the model and Limits for the two hard constraints:

  • A single BSON document may be at most 16 MB. Larger blobs belong in GridFS or object storage.

  • Documents may nest at most 100 levels deep (embedded documents and arrays combined).

The BSON type set

Each type has a canonical name and a numeric alias usable with the $type query operator.

Type Alias Notes

Double

"double"

64-bit IEEE 754 float; the default for a shell literal like 3.14.

String

"string"

UTF-8.

Object

"object"

An embedded document.

Array

"array"

An ordered list; stored as a document with numeric string keys.

Binary data

"binData"

Bytes plus a subtype (e.g. 4 for UUID).

ObjectId

"objectId"

12-byte identifier; see below.

Boolean

"bool"

true / false.

Date

"date"

Milliseconds since the Unix epoch, signed (dates before 1970 are negative).

Null

"null"

An explicit null value.

Regular expression

"regex"

A pattern plus option flags.

32-bit integer

"int"

Int32.

64-bit integer

"long"

Int64 / NumberLong.

Decimal128

"decimal"

128-bit IEEE 754-2008 decimal float; exact base-10, for money.

Timestamp

"timestamp"

Internal replication timestamp; not for application dates.

MinKey / MaxKey

"minKey" / "maxKey"

Sort below / above every other value.

The full list, including deprecated types, is at BSON Types. In mongosh the non-JSON types have constructors:

db.samples.insertOne({
  _id: 1,
  amount:   Decimal128("19.99"),      // exact decimal, not 19.99 as a Double
  count:    Long("9007199254790000"), // 64-bit integer beyond Double's safe range
  when:     ISODate("2026-01-15T00:00:00Z"),
  blob:     BinData(0, "SGVsbG8="),   // subtype 0, base64 payload
  pattern:  /^bson/i
})

Extended JSON

JSON has no syntax for a date or a Decimal128, so MongoDB tools exchange those types using Extended JSON: wrapper objects with $-prefixed keys. The "canonical" mode is type-preserving; "relaxed" mode emits plain JSON numbers and ISO date strings where it can.

{
  "_id": { "$oid": "64f0a1c3e13b1f00a8d9e4b2" },
  "amount": { "$numberDecimal": "19.99" },
  "count": { "$numberLong": "9007199254790000" },
  "when": { "$date": "2026-01-15T00:00:00Z" }
}

mongoexport, mongoimport, and EJSON in mongosh all speak this format; see MongoDB Extended JSON (v2). Tooling that reads and writes these files is covered in Backup & data tools.

_id and ObjectId

Every document has an _id field whose value is unique within its collection and immutable once written. It is always the first field on disk and is automatically backed by a unique index. If you do not supply _id on insert, the driver or server generates an ObjectId.

An ObjectId is 12 bytes:

  • a 4-byte value: seconds since the Unix epoch (the creation time, readable via ObjectId.prototype.getTimestamp());

  • a 5-byte per-process random value;

  • a 3-byte counter that increments per process, starting from a random value.

Because the leading bytes are a timestamp, ObjectId values generated over time are roughly monotonic — they sort close to insertion order, though not strictly so under concurrency or clock skew. See ObjectId() in the manual.

// Server-generated _id.
db.events.insertOne({ kind: "login" })
// { acknowledged: true, insertedId: ObjectId('64f0a1c3e13b1f00a8d9e4b2') }

// Supply your own _id -- any unique, non-array BSON value is allowed.
db.events.insertOne({ _id: "2026-01-15#login", kind: "login" })

// Recover the creation time embedded in an ObjectId.
ObjectId("64f0a1c3e13b1f00a8d9e4b2").getTimestamp()  // ISODate(...)

Embedded documents, arrays, and dot notation

Values can be embedded documents, arrays of scalars, or arrays of documents, nested within the 100-level limit. Dot notation addresses a nested field as a single string: field names joined by ., with a non-negative integer segment selecting an array position.

An annotated BSON document card holding a scalar ObjectId _id, an embedded contact/address document, and an items array of embedded documents, with callouts explaining the 12-byte roughly-monotonic ObjectId and the dot-notation paths contact.address.city and items.0.sku
db.people.insertOne({
  _id: 1,
  name: "Ada Lovelace",
  contact: { email: "ada@example.com", address: { city: "London", zip: "N1 9GU" } },
  items: [ { sku: "A-1", qty: 2 }, { sku: "B-7", qty: 1 } ]
})

// Reach into an embedded document.
db.people.find({ "contact.address.city": "London" })

// Match a specific array position, then any array element.
db.people.find({ "items.0.sku": "A-1" })   // first element only
db.people.find({ "items.sku": "B-7" })     // any element of items

Quote any dotted key in the shell. Dot-notation matching semantics — especially how a query on items.sku matches across array elements, and how $elemMatch differs — are covered in Querying documents. See Dot notation in the manual.

Comparison and sort order across types

When queried values (or values in a sort) have different BSON types, MongoDB orders them by type first, using this fixed order (lowest to highest):

  1. MinKey

  2. Null

  3. Numbers (Int32, Int64, Double, Decimal128 — compared by numeric value, across the four types)

  4. String

  5. Object (embedded document)

  6. Array

  7. BinData

  8. ObjectId

  9. Boolean

  10. Date

  11. Timestamp

  12. Regular expression

  13. MaxKey

The authoritative table is at Comparison/Sort Order. Two consequences worth remembering:

  • Null versus missing. A query { field: null } matches both documents where field holds null and documents where field is absent. To match only an explicit null, combine with a type check: { field: { $type: "null" } }. To match only "present", use { field: { $exists: true } }.

  • Numeric-type conflation. The numeric types sort and compare by value, so 1, 1.0, and NumberLong(1) are equal for query matching and indexing. They are still stored as distinct BSON types, and $type distinguishes them — { n: { $type: "int" } } will not match a value stored as a Double. Choose one numeric type per field, and use Decimal128 for money to avoid binary-float rounding.

db.t.insertMany([ { n: 1 }, { n: 1.0 }, { n: NumberLong(1) }, { n: "1" } ])

db.t.find({ n: 1 }).count()                 // 3 -- the three numeric values, not the string
db.t.find({ n: { $type: "int" } }).count()  // 1 -- only the value stored as Int32
db.t.find({ n: { $lt: "2" } })              // the string "1": strings sort after all numbers