Chapter 02 · BSON, Documents, Collections, Flexible Schema, and Data Types
BSON vs JSON: Binary Representation, Type Fidelity, Size, and Driver Mapping
Separate BSON storage, JSON text, Extended JSON, and driver-native values, then prove which type information survives each round trip.
Learning outcomes
AtlasMart's product API accepts familiar JSON, but its database must preserve money, timestamps, identifiers, binary thumbnails, and large counters without silently changing meaning. A JSON-shaped screen payload and a MongoDB document can look nearly identical while carrying different type information. This lesson establishes the boundary precisely: BSON is MongoDB's binary data representation; JSON and Extended JSON are textual representations; drivers map language-native values to and from BSON.
Distinguish BSON storage from plain JSON text, mongosh display forms, and Extended JSON v2.
Explain Canonical versus Relaxed Extended JSON and identify where readability can sacrifice exact type fidelity.
Trace Python/PyMongo values through BSON encoding and decoding, including ObjectId, Date, Int64, Decimal128, and Binary.
Measure encoded BSON size instead of estimating document size from pretty-printed JSON.
Diagnose a serializer that stringifies BSON-specific values and prove the repaired round trip preserves types.
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. BSON is binary typed data, not compressed JSON text
BSON (“Binary JSON”) stores an ordered document
as typed elements. Each element carries a BSON type tag, a
null-terminated field name, and type-specific bytes. A simple
string document therefore includes framing and field-name
overhead; BSON was designed for efficient traversal and rich
types, not as a universal compression format. The practical
benefit is semantic: MongoDB can distinguish an integer from a
double, a UTC date from a string, a Decimal128 from
a binary floating-point value, and an ObjectId from
24 ordinary hexadecimal characters.
Plain JSON has objects, arrays, strings, numbers, booleans, and null. It does not have a native Date, ObjectId, Binary, Int32/Int64 distinction, Decimal128, Timestamp, or BSON regular-expression type. If an application serializes those values through ordinary JSON without an agreed encoding, information is lost or converted to strings/numbers whose semantics must be guessed later.
| Logical value | BSON/native representation | Naive JSON risk |
|---|---|---|
| Product identifier | ObjectId(...) |
Becomes a string; no longer has ObjectId type semantics |
| Money | Decimal128("899.90") |
May become an IEEE-754 double and lose decimal intent/precision |
| Event time | BSON Date (UTC milliseconds) | Becomes an arbitrary string unless format/timezone contract is explicit |
| Large counter | Int64 | May exceed safe integer precision in JavaScript/JSON consumers |
| Binary thumbnail/hash | BinData/Binary subtype | Requires explicit base64 + subtype convention |
2. Extended JSON is a transport convention for BSON types
MongoDB Extended JSON v2 adds JSON objects such
as {"$oid":"..."}, {"$date":...}, and
{"$numberDecimal":"..."} so BSON values can cross a
textual JSON boundary.
Canonical Extended JSON favors exact type
preservation, including wrappers for numeric widths.
Relaxed Extended JSON favors
readable/interoperable JSON and can render some numeric/date
values without wrappers when doing so is considered safe. That
makes Relaxed form useful for people and APIs, but it is not a
promise that every BSON type round-trips through every generic
JSON parser unchanged.
mongosh also presents convenience constructors such
as ObjectId(), ISODate(), and
Decimal128()/NumberDecimal(). Those
are shell representations—not bytes stored literally inside
WiredTiger files.
use atlasmartconst d = { _id: ObjectId("66b4f0b64e4e6a1234567890"), sku: "sku-camera-pro", price: Decimal128("1999.90"), sold: Long("9007199254740993"), capturedAt: ISODate("2026-09-02T05:30:00Z"), digest: BinData(0, "AQIDBA==")};print(EJSON.stringify(d, null, 2, {relaxed:false}));print(EJSON.stringify(d, null, 2, {relaxed:true}));
The canonical output should expose explicit wrappers for BSON-specific types. The relaxed output is easier to read but should not be treated as a universal lossless interchange format. Always choose the serialization contract intentionally.
3. A driver performs type mapping on both sides of the wire
PyMongo represents a BSON document primarily as a Python
dict, but special BSON values use classes from the
bson package distributed with PyMongo. Python
int values are encoded as BSON Int32 when they fit
and Int64 when required; Python float maps to BSON
double; datetime.datetime maps to BSON Date;
Decimal128, ObjectId, and
Binary carry their own semantics. A plain Python
decimal.Decimal is not automatically the same as
BSON Decimal128 unless you use the supported conversion path.
# Create a clean virtual environment first.python -m venv .venv# PowerShell: .\.venv\Scripts\Activate.ps1# bash/zsh: source .venv/bin/activatepython -m pip install "pymongo==4.17.0"
from datetime import datetime, timezoneimport jsonfrom bson import BSON, Binary, Decimal128, Int64, ObjectIdfrom bson.json_util import dumps, loads, CANONICAL_JSON_OPTIONSdoc = { "_id": ObjectId("66b4f0b64e4e6a1234567890"), "sku": "sku-camera-pro", "price": Decimal128("1999.90"), "sold": Int64(9007199254740993), "capturedAt": datetime(2026, 9, 2, 5, 30, tzinfo=timezone.utc), "digest": Binary(b""),}raw = BSON.encode(doc)round_trip = BSON(raw).decode()print("encoded_bytes=", len(raw))for key, value in round_trip.items(): print(key, type(value).__name__, repr(value))canonical = dumps(doc, json_options=CANONICAL_JSON_OPTIONS)restored = loads(canonical)print("canonical=", canonical)print("restored_types=", {k: type(v).__name__ for k, v in restored.items()})# Deliberately wrong: force unsupported objects to strings.lossy = json.dumps(doc, default=str)print("lossy=", lossy)print("lossy_types=", {k: type(v).__name__ for k, v in json.loads(lossy).items()})
Expected evidence
-
encoded_bytesis the BSON byte length, not the character count of pretty JSON. -
The BSON round trip returns
ObjectId,Decimal128,Int64(or an integer representation preserving the stored width as defined by the driver),datetime, and binary bytes/Binary semantics rather than arbitrary strings. -
Canonical Extended JSON contains type wrappers such as
$oid,$numberDecimal,$numberLong,$date, and$binary. -
The
json.dumps(..., default=str)path produces strings for values it cannot represent. Parsing that JSON cannot reconstruct the original BSON types without out-of-band rules.
This proves a serialization boundary can change types. It does not prove every API should expose Extended JSON; many APIs intentionally map database types into a domain-specific contract. The requirement is to make that mapping explicit and test it.
4. Server lab: store the typed document and inspect its size
Start a fresh standalone server on host port 27022,
insert one typed AtlasMart product, then use $type,
$bsonSize, and EJSON output as independent
evidence.
docker rm -f atlasmart-mongo-ch02-l1 2>/dev/null || truedocker run --name atlasmart-mongo-ch02-l1 -p 127.0.0.1:27022:27017 -d mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim
mongosh "mongodb://127.0.0.1:27022/atlasmart?directConnection=true" --quiet --eval 'db.bson_lab.drop();db.bson_lab.insertOne({ _id:ObjectId("66b4f0b64e4e6a1234567890"), sku:"sku-camera-pro", price:Decimal128("1999.90"), sold:Long("9007199254740993"), capturedAt:ISODate("2026-09-02T05:30:00Z"), digest:BinData(0,"AQIDBA==")});printjson(db.bson_lab.aggregate([ {$project:{_id:0, types:{ price:{$type:"$price"}, sold:{$type:"$sold"}, capturedAt:{$type:"$capturedAt"}, digest:{$type:"$digest"} }, bytes:{$bsonSize:"$$ROOT"}}}]).toArray());print(EJSON.stringify(db.bson_lab.findOne(), null, 2, {relaxed:false}));'
The exact byte count depends on the exact field names and
values. The stable evidence is that the server reports the BSON
types you inserted and computes encoded BSON size. Clean up with
docker rm -f atlasmart-mongo-ch02-l1.
5. Production judgment: serialization is part of the data contract
Do not allow database-specific values to drift through a generic JSON serializer accidentally. Decide which layer owns conversion. A public REST API might intentionally expose an ObjectId as a string, money as a decimal string plus currency, and dates as RFC 3339 UTC strings. That can be an excellent contract—as long as the application reconstructs database-native values deliberately and validates range/precision/timezone rules. Internal event streams may choose Canonical Extended JSON when lossless BSON round-trip matters more than readability.
Store money with an exact decimal representation when the business rule requires decimal arithmetic; do not assume binary floating point is acceptable. Store event times in UTC and preserve timezone context outside the BSON Date when the original zone has business meaning. Treat large integers crossing JavaScript/JSON boundaries carefully. Measure encoded BSON size when documents approach limits.
The next lesson moves from value types to document structure:
_id, ObjectId behavior, field names, embedded
documents, arrays, the 16 MiB size ceiling, and nesting limits.
Verification checklist
- You can explain why BSON and JSON are not synonyms.
- You can choose Canonical or Relaxed Extended JSON based on the required fidelity.
-
You can prove BSON byte size with an encoder or
$bsonSize. - You have a regression test for ObjectId/Date/Decimal128/Int64 serialization at every external JSON boundary.
- You do not claim that pretty-printed mongosh syntax is the physical storage format.
Check your understanding
- Why can ordinary JSON not losslessly represent every BSON document?
- What is the goal of Canonical Extended JSON?
- Why is json.dumps(..., default=str) dangerous for database round trips?
- Does a smaller JSON string imply a smaller BSON document?
- When is exposing ObjectId as a string acceptable?
Review the answers
JSON has a smaller type system. BSON adds types such as ObjectId, Date, Decimal128, Int32/Int64 distinctions, Binary, Timestamp, and regular expressions; a convention such as Extended JSON is needed to preserve them in JSON text.
Type preservation. It uses explicit wrappers even when a shorter/readable representation exists, so the BSON type can generally be reconstructed.
It makes unsupported values serializable by converting them to strings, but removes the information needed to know that a value was originally an ObjectId, Date, Decimal128, Binary, or another typed value.
No. JSON character length and BSON encoded byte length are different representations with different framing/type overhead. Measure BSON size directly.
When the API contract intentionally defines it as an opaque string and the application validates/reconstructs it where needed. The problem is accidental conversion, not deliberate domain mapping.
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.
-
PyMongo Extended JSON
— Official
bson.json_utilExtended JSON serialization and parsing. - $bsonSize — Official server expression for BSON-encoded document size.
- PyMongo release notes — Official current driver release line; 4.17 is current at review time.