MongoDB Extended JSON
What comes out of MongoDB is not JSON. ObjectId("…") is not valid JSON, and neither is {"$date":{"$numberLong":"1600000000000"}} to anything downstream that expects a timestamp. This reads all three dialects — shell output, Extended JSON v2 canonical and relaxed — and writes whichever one you need, keeping the type information rather than flattening it by accident.
How to use it
- Paste a document or an array of them. The input dialect is worked out for you — you do not pick it.
- Choose the output. Plain JSON for anything outside the Mongo ecosystem; relaxed for a readable dump; canonical when a driver or a test fixture has to round-trip exactly; mongosh syntax to paste back into a shell.
- Read the status line. It counts the BSON types it found — 3 ObjectId, 2 Date, 1 Decimal128 — and warns whenever a value could not survive the conversion exactly.
The three dialects, and why there are three
BSON has types JSON does not: an ObjectId, a 64-bit integer, a Decimal128, a date, a binary blob, a regex with flags. Extended JSON is the agreed way to write them in JSON, and it comes in two modes.
- Canonical keeps every type, always:
{"n":{"$numberInt":"42"}}. Ugly, and it round-trips through a driver without losing a thing. This is the form to use for fixtures and for anything a program will read back. - Relaxed writes what JSON can express natively and only wraps what it cannot:
{"n":42}, dates as{"$date":"2020-09-13T12:26:40Z"}. Readable, and lossy — a relaxed42could have been an int32, an int64 or a double. - mongosh syntax is neither: it is JavaScript.
ObjectId("…"),ISODate("…"),NumberLong("…"), bareMinKey, unquoted keys, single quotes, trailing commas,/regex/iliterals. It is what you get when you copy out of a shell or a log, and it is the reasonJSON.parseis throwing at you.
What it reads
All of these arrive as the same typed document, whichever way they were written:
$oid,$date(ISO string,$numberLong, or the v1 plain number),$numberInt,$numberLong,$numberDouble(including"Infinity"and"NaN"),$numberDecimal.$binaryin both spellings — v2{base64, subType}and the v1{"$binary":"…","$type":"00"}that older dumps still use.$regularExpression, and the v1$regex/$optionspair.$timestamp,$minKey,$maxKey,$undefined,$symbol,$codewith or without$scope, and DBRefs ($ref/$id/$db).- Shell constructors:
ObjectId,ISODate,Date,NumberLong,NumberInt,NumberDecimal,Timestamp,BinData,UUID,DBRef,Code,MinKey,MaxKey— with or without parentheses where the shell omits them.
The scan that does this is string-aware: a document whose note field contains the text ObjectId("x") comes out with that text intact. Rewriting blindly with a regular expression is the obvious way to build this tool and it corrupts exactly the documents that mention their own ids.
Where a conversion cannot be exact — and what happens instead
Two BSON types do not fit in a JSON number, and this is the part other converters get quietly wrong.
- 64-bit integers. A JSON number is a double: anything past 253 is rounded.
{"$numberLong":"9007199254740993"}becomes9007199254740992— one less than it was, silently. By default such values are written as strings in plain JSON, which is exact, and the status line says how many. Switch to JSON numbers only when you know the values are small. - Decimal128. 34 significant digits, decimal, exact — the reason money is stored in it. Turning
9.99into a binary double is precisely the error the type exists to prevent, so plain output keeps the digits as a string. - Infinity and NaN have no JSON spelling. Plain output writes them as the strings
"Infinity"and"NaN"and counts them.
Going the other way, plain JSON carries no types, so the extended dialects have to infer: an integer within int32 range becomes $numberInt, a larger one $numberLong, anything with a fractional part $numberDouble. Strings are left alone unless you tick the two detect options — because a 24-character hex string is often a hash, and turning it into an ObjectId would be a guess with consequences.
Examples
Shell output in:
{ _id: ObjectId("507f1f77bcf86cd799439011"),
created: ISODate("2020-09-13T12:26:40Z"),
price: NumberDecimal("9.99"),
tags: /^sale/i }
Plain JSON out:
{
"_id": "507f1f77bcf86cd799439011",
"created": "2020-09-13T12:26:40.000Z",
"price": "9.99",
"tags": "/^sale/i"
}
Canonical Extended JSON out — the same document, ready to load back:
{
"_id": { "$oid": "507f1f77bcf86cd799439011" },
"created": { "$date": { "$numberLong": "1600000000000" } },
"price": { "$numberDecimal": "9.99" },
"tags": { "$regularExpression": { "pattern": "^sale", "options": "i" } }
}
FAQ
Is my data uploaded?
No. Everything runs in this page — the parser, the type inference and the writer are all local, and no library is fetched for them. Production documents are exactly the thing you should not paste into a server-side converter.
Can it handle a whole mongoexport file?
A JSON array, yes. mongoexport's default output is one document per line, which is JSONL rather than JSON — convert that with JSONL to JSON first, or paste one line at a time.
Why is my date one hour out?
It is not — it is in UTC. BSON dates are milliseconds since the epoch with no timezone, so both the ISO string and the epoch number here are UTC. A local-time rendering is your application's decision, not the document's.
What does the shell output do with a Decimal128?
Writes NumberDecimal("9.99"), which is what you would have typed. Paste it into mongosh and the type survives; paste a bare 9.99 and you have inserted a double.
My document has a key called $set. Does that break it?
No. A wrapper is only recognised when the object has exactly the shape the spec defines — {"$oid": "…"} alone, for example. An update document full of $set and $inc passes through as an ordinary object.