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.

Beginner100–125 minutesBSON/EJSON + PyMongo round-trip labMongoDB Community Server 8.3.8 · mongosh 2.10.0 · PyMongo 4.17.0Last reviewed: September 2026

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.

01

Distinguish BSON storage from plain JSON text, mongosh display forms, and Extended JSON v2.

02

Explain Canonical versus Relaxed Extended JSON and identify where readability can sacrifice exact type fidelity.

03

Trace Python/PyMongo values through BSON encoding and decoding, including ObjectId, Date, Int64, Decimal128, and Binary.

04

Measure encoded BSON size instead of estimating document size from pretty-printed JSON.

05

Diagnose a serializer that stringifies BSON-specific values and prove the repaired round trip preserves types.

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. 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.

mongosh · compare Canonical and Relaxed Extended JSON
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.

shell · pin the official Python driver
# 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"
Python · BSON bytes, exact round trip, and deliberately lossy JSON
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_bytes is 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.

bash · start disposable Lesson 1 server
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 · inspect stored types and encoded size
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

  1. Why can ordinary JSON not losslessly represent every BSON document?
  2. What is the goal of Canonical Extended JSON?
  3. Why is json.dumps(..., default=str) dangerous for database round trips?
  4. Does a smaller JSON string imply a smaller BSON document?
  5. 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

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.