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.
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.
Distinguish BSON Int32, Int64, double, and Decimal128 and choose them from domain semantics rather than convenience.
Store application datetimes in UTC and distinguish BSON Date from BSON Timestamp.
Explain Binary subtypes and the difference between a stored
BSON regex value and a query $regex predicate.
Reuse ObjectId safely as an identifier without treating its timestamp as business time.
Prove explicit null versus missing-field behavior with
$type, $exists, and MongoDB 8.x
query semantics.
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.
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.
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.
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.
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.
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 "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:10matches only document 1.$exists:falsematches only document 2.-
The aggregation
$typereports"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
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
- Why is Decimal128 often preferable to double for decimal money?
- What is the difference between BSON Date and BSON Timestamp?
- What does {field:null} match?
- How do you query only explicit null?
- 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
- MongoDB release notes — Official current stable server series and patch notes.
- MongoDB 8.3 release notes — Official 8.3 changes; 8.3.8 is the latest released patch at review time and 8.3.9 is upcoming.
- MongoDB Extended JSON v2 — Canonical and Relaxed Extended JSON representations and type-preservation rules.
- BSON types — Official BSON type definitions and ObjectId notes.
- PyMongo BSON data formats — Official Python driver mapping between Python dictionaries/types and BSON.
- BSON comparison/sort order — Official BSON type ordering and type-bracketing context.
- $exists — Official field-presence semantics, including null values.
-
$type aggregation expression
— Official type inspection including
missing. - Migrate undefined data and queries — Official MongoDB 8.0 change to null/undefined comparison behavior.
- PyMongo dates and times — Official UTC/aware/naive datetime mapping guidance.