Chapter 02 · BSON, Documents, Collections, Flexible Schema, and Data Types

Numeric, Date, Timestamp, Decimal128, Binary, Regex, ObjectId, and Null Semantics

Choose BSON numeric/time/binary/regex/null types from domain semantics and prove the null-versus-missing behavior that affects every later query.

Beginner105–130 minutesBSON type matrix + null/missing truth-table labMongoDB Community Server 8.3.8 · mongosh 2.10.0 · PyMongo 4.17.0Last reviewed: September 2026

Learning outcomes

AtlasMart's checkout, telemetry, and catalog data contain values that all look like “numbers,” “times,” or “empty values” at first glance. Those categories are too coarse. A double can round money, an Int64 can cross a JSON/JavaScript precision boundary, a BSON Date and BSON Timestamp solve different problems, and a missing field is not the same state as an explicit null. This lesson makes each type observable.

01

Distinguish BSON Int32, Int64, double, and Decimal128 and choose them from domain semantics rather than convenience.

02

Store application datetimes in UTC and distinguish BSON Date from BSON Timestamp.

03

Explain Binary subtypes and the difference between a stored BSON regex value and a query $regex predicate.

04

Reuse ObjectId safely as an identifier without treating its timestamp as business time.

05

Prove explicit null versus missing-field behavior with $type, $exists, and MongoDB 8.x query semantics.

Chapter 02 reproducible baseline

Mandatory server examples use a disposable loopback-only standalone mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim. Driver examples pin pymongo==4.17.0. The lab is intentionally unauthenticated only because it is short-lived and published to 127.0.0.1; Chapter 01 already demonstrated the authenticated reusable lab. Do not expose this container on an untrusted interface. Atlas is optional and not required.

Generation-time execution note

The build environment does not provide Docker, mongod, or mongosh, and cannot install PyMongo from the network. Product/driver commands were reviewed against current official documentation but were not executed here. Expected-output blocks describe stable evidence shape, not fabricated captured output. Learners should record their actual versions and outputs.

1. “Number” is a family of BSON types

BSON defines 32-bit integers, 64-bit integers, IEEE-754 double precision floating point, and Decimal128. MongoDB compares numeric types as numbers in many query contexts, but representation still matters for range, exactness, serialization, storage, and driver mapping. PyMongo encodes ordinary Python integers as Int32 or Int64 according to range and Python floats as BSON double. Monetary values that require decimal semantics should use Decimal128 (or an integer minor-unit model with a documented scale), not an unexamined double.

The classic failure is 0.1 + 0.2: binary floating point cannot represent those decimal fractions exactly. A more dangerous cross-system failure is a 64-bit integer larger than JavaScript's safe integer range (253−1) being serialized as an ordinary JSON number and rounded by a consumer. Canonical Extended JSON can preserve an Int64 using $numberLong.

mongosh · numeric type evidence
use atlasmartdb.type_lab.drop();db.type_lab.insertOne({  _id:"numeric",  quantity:Int32(7),  lifetimeSales:Long("9007199254740993"),  ratio:0.1 + 0.2,  money:Decimal128("0.30")});printjson(db.type_lab.aggregate([{$project:{  _id:0,  values:"$$ROOT",  types:{quantity:{$type:"$quantity"}, lifetimeSales:{$type:"$lifetimeSales"}, ratio:{$type:"$ratio"}, money:{$type:"$money"}}}}]).toArray());print(EJSON.stringify(db.type_lab.findOne({_id:"numeric"}), null, 2, {relaxed:false}));

2. BSON Date is application time; BSON Timestamp is a different primitive

A BSON Date stores a signed 64-bit count of milliseconds relative to the Unix epoch. MongoDB stores dates in UTC. Drivers map language date/time classes to that representation; PyMongo recommends using UTC-aware datetimes and warns that naive datetimes are assumed to be UTC. If AtlasMart needs the shopper's original timezone for legal/business rules, store that zone/offset as separate domain data—the BSON Date itself represents an instant.

BSON Timestamp is a special 64-bit value with seconds and an ordinal increment. MongoDB uses timestamps internally, notably in replication/oplog/change-processing contexts. It is not a drop-in replacement for an application createdAt Date. A timestamp's ordering semantics and lifecycle belong to database mechanisms, not human calendar arithmetic.

Python · UTC-aware datetime boundary
from datetime import datetime, timezonefrom bson import BSONaware = {"createdAt": datetime(2026, 9, 2, 5, 30, tzinfo=timezone.utc)}raw = BSON.encode(aware)print(BSON(raw).decode())# Wrong pattern for application events: naive local wall-clock time.wrong = datetime.now()                 # local time, no tzinforight = datetime.now(timezone.utc)     # explicit UTC instantprint("wrong tzinfo:", wrong.tzinfo)print("right tzinfo:", right.tzinfo)

PyMongo converts aware datetimes to UTC before storage. Treat a naive datetime as hazardous unless the application has deliberately defined “naive means UTC.”

3. Binary, Regex, ObjectId, and Null are not strings

Binary stores bytes plus a subtype. Subtypes communicate conventions such as generic binary or UUID/vector-related encodings; cross-language UUID compatibility requires an explicit representation policy. A stored BSON regular expression value is a data value containing a pattern and options, while the $regex query operator is a predicate used to match strings. Do not use regex as a universal search engine; indexing and anchoring rules matter later.

ObjectId is a 12-byte identifier value, not a 24-character database string even though its hexadecimal representation is commonly shown that way. Null is an explicit BSON type. A field can also be missing, meaning no key exists in that document. That distinction affects updates, validation, application defaults, indexes, and queries.

Python · round-trip BSON-specific values without JSON
from bson import BSON, Binary, Decimal128, Int64, ObjectId, Regex, Timestamporiginal = {    "oid": ObjectId("66d000000000000000000001"),    "i64": Int64(9007199254740993),    "money": Decimal128("19.99"),    "payload": Binary(b"\x01\x02\x03", subtype=0),    "pattern": Regex(r"^atlas", "i"),    "cluster_marker": Timestamp(1760000000, 7),    "empty": None,}raw = BSON.encode(original)round_trip = BSON(raw).decode()for key, value in round_trip.items():    print(f"{key:14} {type(value).__name__:12} {value!r}")print("encoded BSON bytes:", len(raw))

The driver round trip keeps these values as BSON-aware Python types rather than turning them into strings. That is the contrast to a generic JSON serializer: the BSON encoder has an explicit mapping for Binary, Regex, ObjectId, Timestamp, Decimal128, Int64, and null. Exact printed representations can vary by driver version; the Python type names and successful encode/decode round trip are the evidence that matters.

State Document fragment Meaning
Explicit null {middleName:null} Field exists; value is intentionally/currently null
Missing {} No field/value was stored
Empty string {middleName:""} Field exists and is a string with length zero
Deprecated undefined BSON undefined value Legacy/deprecated type; starting MongoDB 8.0, equality-to-null no longer matches undefined values

4. Null versus missing: prove the query, do not guess

The equality predicate {field:null} is intentionally broad: it matches documents where the field is explicitly null or missing. Use $type:10 (or "null") to target explicit BSON null and $exists to distinguish presence/absence. Starting in MongoDB 8.0, equality-to-null no longer also matches the deprecated BSON undefined type; old tutorials can therefore be wrong for modern MongoDB.

shell · start disposable type-semantics lab
docker rm -f atlasmart-mongo-ch02-l3 2>/dev/null || truedocker run --name atlasmart-mongo-ch02-l3 -p 127.0.0.1:27024:27017 -d mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim
mongosh · null/missing truth table
mongosh "mongodb://127.0.0.1:27024/atlasmart?directConnection=true" --quiet --eval 'db.customer_type_lab.drop();db.customer_type_lab.insertMany([ {_id:1,name:"Mina",middleName:null}, {_id:2,name:"Ravi"}, {_id:3,name:"Lena",middleName:""}]);print("eq null:", EJSON.stringify(db.customer_type_lab.find({middleName:null},{_id:1,middleName:1}).sort({_id:1}).toArray()));print("explicit null:", EJSON.stringify(db.customer_type_lab.find({middleName:{$type:10}},{_id:1,middleName:1}).toArray()));print("missing:", EJSON.stringify(db.customer_type_lab.find({middleName:{$exists:false}},{_id:1}).toArray()));print("types:", EJSON.stringify(db.customer_type_lab.aggregate([{$project:{_id:1,t:{$type:"$middleName"}}},{$sort:{_id:1}}]).toArray()));' 

Expected result shape

  • {middleName:null} matches documents 1 and 2 (explicit null + missing).
  • $type:10 matches only document 1.
  • $exists:false matches only document 2.
  • The aggregation $type reports "null", "missing", and "string" for the three documents.

If the actual output differs, record the exact server version and inspect the fixture before changing the explanation.

5. Production judgment: make type choices part of the schema contract

Choose BSON types from business meaning and interoperability. Use Decimal128 or scaled integers for values that require exact decimal arithmetic. Use Int64 deliberately when ranges demand it and ensure downstream JSON/JavaScript systems preserve it. Use UTC Date for application instants; preserve original timezone/locale separately when meaningful. Define Binary subtypes and UUID representation across languages. Treat regex as a specialized type/query tool, not a replacement for search. Define whether null and missing represent different states in the domain.

These choices affect indexes, validators, aggregation operators, encryption schemas, analytics exports, and migration. A schema can be “flexible” while still having a very strict type contract.

The next lesson takes that idea further: how application contracts evolve when old and new document shapes coexist in one collection.

Cleanup

shell · remove disposable Lesson 3 server
docker rm -f atlasmart-mongo-ch02-l3

Verification checklist

  • Numeric fields use intentional BSON types and money avoids unexamined binary floating-point semantics.
  • Application event times are written as UTC Date values, not BSON Timestamp.
  • Binary/regex/ObjectId values are not silently stringified at JSON boundaries.
  • You can distinguish explicit null, missing, and empty string with reproducible queries.
  • You recorded MongoDB 8.x behavior rather than relying on pre-8.0 undefined/null assumptions.

Check your understanding

  1. Why is Decimal128 often preferable to double for decimal money?
  2. What is the difference between BSON Date and BSON Timestamp?
  3. What does {field:null} match?
  4. How do you query only explicit null?
  5. Why can an Int64 be damaged by an ordinary JSON boundary?
Review the answers

Decimal128 represents decimal values with decimal floating-point semantics, avoiding the binary representation error inherent in values such as 0.1. The domain still needs explicit rounding/scale/currency rules.

Date represents an application time instant as milliseconds from the Unix epoch. Timestamp is a special seconds-plus-increment value used by MongoDB mechanisms such as replication; it is not an application datetime replacement.

Documents where the field is explicitly BSON null and documents where the field is missing. Starting in MongoDB 8.0 it no longer also matches the deprecated BSON undefined type.

Use a type predicate such as {field:{$type:10}} (BSON null type). Use $exists when presence itself matters.

JSON has one generic number syntax and JavaScript Number cannot exactly represent every 64-bit integer. Use an explicit string/domain mapping or Extended JSON wrapper when exact Int64 fidelity is required.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.