How to unflatten flat json into nested mongodb documents
- Step 1Prepare the flat dot-notation document — Produce one flat JSON object with dot-separated keys from your ETL output, analytics export, or SQL intermediate. The tool reads a single document object per run.
- Step 2Set the Key delimiter to dot — Leave the Key delimiter as
.so each dot becomes an embedding level. If your intermediate used underscores, set it to_instead. - Step 3Drop or paste the document and run — Drop the
.jsonfile (free tier up to 2 MB) and click Unflatten JSON. Processing is local; the document is never uploaded. - Step 4Convert array-shaped fields by hand — Where the export used numeric segments (
items.0.sku,items.1.sku), the result is{ items: { "0": {...}, "1": {...} } }. Convert those object-of-index fields to real arrays so MongoDB stores them as BSON arrays (and so$unwind/$elemMatchwork). - Step 5Cast special BSON types in your import script — Wrap
_idstrings withObjectId(), date strings withnew Date()orISODate, and high-precision numbers withDecimal128as needed — the unflattener keeps them as plain values. - Step 6Write the document — For one document:
db.collection.insertOne(nested). For many, run each document's nested object through a bulk write loop, or save as NDJSON (one object per line) andmongoimport.
How flat MongoDB exports unflatten
Behaviour from the unflattener logic. The critical row is the numeric-segment case: indexed keys rebuild as objects, not BSON arrays.
| Flat key | Output | MongoDB note |
|---|---|---|
"address.city": "London" | { address: { city: "London" } } | Embedded subdocument — exactly right |
"items.0.sku": "A1", "items.1.sku": "B2" | { items: { "0": { sku: "A1" }, "1": { sku: "B2" } } } | Object keyed "0"/"1", not a BSON array — convert manually |
"_id": "507f1f77bcf86cd799439011" | { _id: "507f1f77bcf86cd799439011" } | Stays a string — wrap with ObjectId() on import |
"createdAt": "2026-05-01T00:00:00Z" | { createdAt: "2026-05-01T00:00:00Z" } | String — wrap with new Date() for a BSON Date |
"price": "19.99" | { price: "19.99" } | Still a string — cast to number or Decimal128 if needed |
Post-unflatten cleanup for a clean import
What to fix after unflattening, because the tool restores shape but not BSON types.
| Field type | Comes out as | Fix before import |
|---|---|---|
| Array of subdocuments | Object keyed "0", "1" | Convert object-of-index to a real array |
| ObjectId | Plain string | Wrap with ObjectId(str) |
| Date / timestamp | ISO string | Wrap with new Date(str) |
| Numeric value flattened from CSV | String | Cast with Number() or Decimal128 |
| Null / empty fields | null preserved | Strip with JSON Null Stripper if undesired |
Cookbook
Real before/after rows from flattened MongoDB exports. Identifiers anonymised; delimiter shown is the Key delimiter field value.
Rebuild an embedded address subdocument
ExampleAn analytics pipeline flattened the address map. The default dot delimiter restores the embedded subdocument MongoDB stores natively.
Flat input:
{
"name": "Ada",
"address.city": "London",
"address.zip": "EC1A",
"profile.settings.theme": "dark"
}
Output:
{
"name": "Ada",
"address": { "city": "London", "zip": "EC1A" },
"profile": { "settings": { "theme": "dark" } }
}Array of subdocuments comes back as an object — convert it
ExampleThe most important MongoDB caveat: indexed keys rebuild as an object keyed by index, not a BSON array. Convert before insert so aggregation operators work.
Flat input:
{ "items.0.sku": "A1", "items.0.qty": "2", "items.1.sku": "B2" }
Output:
{ "items": { "0": { "sku": "A1", "qty": "2" }, "1": { "sku": "B2" } } }
Fix in code:
doc.items = Object.values(doc.items); // → real array_id and dates stay strings — cast on import
ExampleThe tool preserves identifiers and timestamps as strings. Wrap them in your import script for proper BSON types.
Output: { "_id": "507f1f77bcf86cd799439011",
"createdAt": "2026-05-01T00:00:00Z" }
Import script:
doc._id = new ObjectId(doc._id);
doc.createdAt = new Date(doc.createdAt);
db.users.insertOne(doc);An $set-style update path rebuilds into an object
ExampleIf your source mirrored MongoDB update syntax with dotted paths, unflattening turns those paths into the nested object you would set.
Flat input (mirrors $set: { 'address.city': 'Paris' }):
{ "address.city": "Paris", "address.country": "FR" }
Output:
{ "address": { "city": "Paris", "country": "FR" } }
// Use as the full embedded doc, or as the $set target value.Bulk restore via NDJSON and mongoimport
ExampleUnflatten each document, fix arrays/types, then write one JSON object per line and import.
# After unflattening + fixing each doc, build NDJSON:
# restored.ndjson (one document per line)
{"name":"Ada","address":{"city":"London"}}
{"name":"Lin","address":{"city":"Paris"}}
mongoimport --db shop --collection users --file restored.ndjsonErrors and edge cases
Real errors and silent failures sourced from each platform's own documentation. Match the wording to the row, fix what the row says to fix.
Array of embedded documents (items.0.sku, items.1.sku)
Object, not BSON arrayRebuilds as { items: { "0": {...}, "1": {...} } }, keyed by index. Convert to a real array (e.g. Object.values) before insert so $unwind, $elemMatch, and $lookup behave as expected.
ObjectId stored as a string
Preserved as string_id comes through as a plain string. Wrap it with new ObjectId(str) in your import script if the field must be a real ObjectId.
Date / timestamp value
Preserved as stringISO date strings stay strings — no Date casting. Wrap with new Date(str) on import for a BSON Date.
Numeric value flattened from CSV ("19.99")
Stays stringNo type coercion. Cast with Number() or Decimal128 before insert. The JSON Format Fixer repairs syntax but does not cast types.
Delimiter appears in a field name
Over-splitsA key like geo.lat.long over-nests if a segment legitimately contains the delimiter. Choose a non-colliding delimiter or rename with the JSON Key Renamer.
Top-level field collides with a dotted path
Last write winsIf both address (scalar) and address.city exist, the later key wins. Reorder or rename to keep the intended field.
Input is an array of documents
Not the expected inputThe tool unflattens one document object per run. For a collection, process each document separately, then assemble NDJSON for mongoimport.
Malformed JSON from an ETL dump
Parse errorInput must be valid JSON. Repair quotes, trailing commas, or unquoted keys with the JSON Format Fixer before unflattening.
Document over the free 2 MB limit
Blocked on freeThe free JSON tier caps files at 2 MB. Upgrade to Pro to remove the file-size limit, or restore documents in smaller batches.
Frequently asked questions
Does the unflattener rebuild arrays of embedded documents from indexed keys?
No. Indexed keys like items.0.sku rebuild as an object keyed by index — { items: { "0": {...}, "1": {...} } } — not a BSON array. This is the most important MongoDB caveat: convert those object-of-index fields to real arrays (for example Object.values) before insert so aggregation operators work.
How are ObjectId values handled?
They are preserved as strings. A flattened _id like "507f1f77bcf86cd799439011" stays a string. Wrap it with new ObjectId(str) in your import script if the field must be a true ObjectId; the tool does not cast it.
Does it cast date strings to BSON Dates or numeric strings to numbers?
No. Values pass through unchanged. ISO date strings stay strings and numeric-looking strings stay strings. Cast them in your import code (new Date(), Number(), Decimal128) — the JSON Format Fixer fixes JSON syntax but does not change value types.
Can I unflatten a whole collection export at once?
No. The tool reads one flat document object per run. Process documents individually, fix arrays and types, then assemble them into NDJSON (one JSON object per line) for mongoimport.
Does it work with MongoDB $set update dot-notation?
Yes, indirectly. Dotted paths that mirror $set: { 'address.city': ... } rebuild into the nested object { address: { city: ... } }. Use that as the full embedded document or as the value side of your update — but remember it produces objects, not arrays, for numeric segments.
Is the MongoDB document data transmitted to JAD Apps?
No. Unflattening runs entirely in your browser. Document data, user profiles, and any PII are never uploaded to JAD Apps servers.
Which delimiter should I pick for MongoDB exports?
Keep the default . for dot-notation exports. If your SQL intermediate used underscores, set the delimiter to _. The field accepts up to 3 characters.
What happens if a top-level field collides with a dotted path?
Last write wins — a scalar parent is overwritten by an object (or vice versa) depending on key order. The result is deterministic for a given order; reorder or rename to keep the field you need.
Can I control the output indentation?
No. Output is always 2-space indented JSON. For NDJSON / mongoimport, minify each document to one line with the JSON Minifier.
How do I flatten a nested MongoDB document the other way?
Use the JSON Flattener. Note that flattening arrays in dot mode produces numeric segments, which this unflattener turns back into index-keyed objects rather than arrays.
My insert fails on array operators after unflattening — why?
Almost always because an array field is still an object keyed "0", "1". Convert those fields to real arrays before insert. Validate the final document with the JSON Validator.
What is the maximum document size on the free tier?
2 MB per file on the free JSON tier. Pro removes the file-size limit. The tool itself is a Pro tool.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.