How to unflatten flat json into nested firestore documents
- Step 1Export the document as a flat JSON object — From a BigQuery Firestore export, an Elasticsearch dump, or a migration script, produce a single flat JSON object whose keys are dot-paths:
{"address.city": "London", "profile.preferences.theme": "dark"}. The tool reads one object per run, not an array of documents. - Step 2Set the Key delimiter to match your export — If your keys use dots (
address.city), leave the Key delimiter field as.. If a tool flattened with underscores (address_city), type_instead. Only this single delimiter is used — there is no multi-separator or bracket-notation mode. - Step 3Drop the file or paste the JSON, then run — Drop a
.jsonfile (free tier up to 2 MB) onto the dropzone and click Unflatten JSON. Processing happens entirely in your browser; the file is never uploaded. - Step 4Check the rebuilt map shape — Confirm the nesting matches the document you expect. Watch for keys that were split on a delimiter that also appears inside a field name (e.g. a literal dot in a value-derived key) — those over-split into extra map levels.
- Step 5Fix any array-shaped fields by hand — If the export used numeric segments (
tags.0,tags.1), the output is{ tags: { "0": ..., "1": ... } }— a map, not an array. Firestore arrays need a real JSON array, so convert those few fields manually before writing. - Step 6Write the document to Firestore — Copy the output or download
<name>.nested.json, thenadmin.firestore().doc('users/uid123').set(nestedObject). For many documents, loop the SDK over your per-document nested objects.
What unflattening does to a Firestore export
Behaviour observed directly from the unflattener logic (lib/json-unflattener.ts). The tool splits each top-level key on the delimiter and assigns the value at the resulting path; intermediate levels are always plain objects.
| Flat input key | Output structure | Firestore note |
|---|---|---|
"address.city": "London" | { address: { city: "London" } } | Map nesting — exactly what .set() expects |
"profile.preferences.theme": "dark" | { profile: { preferences: { theme: "dark" } } } | Any depth supported; each segment is a map key |
"tags.0": "vip", "tags.1": "beta" | { tags: { "0": "vip", "1": "beta" } } | Map with string keys, not a Firestore array — convert manually |
"createdAt": "2026-05-01T00:00:00Z" | { createdAt: "2026-05-01T00:00:00Z" } | String preserved verbatim — wrap as Timestamp in your import code |
"meta.score": "5432" | { meta: { score: "5432" } } | Still a string — no numeric coercion happens here |
Tool options and limits
The UI exposes exactly one control. Free-tier limits come from the JSON family limits (lib/tier-limits.ts); the unflattener is a Pro tool.
| Setting | Value / range | Notes |
|---|---|---|
| Key delimiter | 1–3 characters, default . | The only UI control; splits every key on this exact string |
| Output indentation | Fixed 2-space | Not user-adjustable from the UI |
| Input shape | One flat JSON object | An array of documents is not the expected input — process one object per run |
| Free file size | 2 MB | Pro removes the file-size limit |
| Privacy | 100% client-side | Nothing is uploaded to JAD Apps servers |
Cookbook
Real before/after rows from flattened Firestore documents. Identifiers anonymised; the delimiter shown is the one you would set in the Key delimiter field.
Rebuild a user document from a BigQuery Firestore export
ExampleBigQuery's Firestore export flattens map fields to dot-paths. Set the delimiter to . and unflatten to get the map shape .set() wants.
Flat input:
{
"displayName": "Ada",
"address.city": "London",
"address.country": "UK",
"profile.preferences.theme": "dark"
}
Output (.nested.json):
{
"displayName": "Ada",
"address": { "city": "London", "country": "UK" },
"profile": { "preferences": { "theme": "dark" } }
}Underscore-flattened export needs the delimiter changed
ExampleA SQL intermediate table named columns with underscores. Leaving the delimiter as . would do nothing useful — set it to _ so each underscore becomes a map level.
Key delimiter: _
Flat input:
{ "address_city": "Berlin", "address_zip": "10115" }
Output:
{ "address": { "city": "Berlin", "zip": "10115" } }Numeric segments do NOT become a Firestore array
ExampleA flattened array field uses tags.0, tags.1. The tool rebuilds these as object keys, not as an array — the single most common Firestore surprise.
Flat input:
{ "tags.0": "vip", "tags.1": "beta" }
Output:
{ "tags": { "0": "vip", "1": "beta" } }
For Firestore you want: "tags": ["vip", "beta"]
→ convert that field by hand before .set()Top-level scalar colliding with a nested path
ExampleIf a flat object has both a and a.b, the result depends on key order: the later key wins because a non-object parent is overwritten with a fresh object.
Input A: { "a": 1, "a.b": 2 } → { "a": { "b": 2 } }
Input B: { "a.b": 2, "a": 1 } → { "a": 1 }
Reorder or rename the colliding key to keep the field you need.Write the rebuilt document with the Admin SDK
ExampleOnce the nested JSON looks right, write it as a single Firestore document. Wrap timestamps and references in your import script since the tool keeps them as strings.
const doc = require('./user.nested.json');
await admin.firestore()
.doc('users/uid123')
.set(doc);
// timestamps came through as strings:
// admin.firestore.Timestamp.fromDate(new Date(doc.createdAt))Errors 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.
Numeric segments (tags.0, tags.1) in the flat keys
By designRebuilt as object keys "0", "1" inside a map, never as a Firestore array. Convert those specific fields to real arrays before writing if Firestore array semantics matter.
Input is an array of flat documents, not one object
Not the expected inputThe tool reads a single top-level flat object per run. Feed it one document at a time; an array iterates by numeric index and produces unexpected output.
Delimiter character also appears inside a field name
Over-splitsA key like version.1.2.label with delimiter . splits into four levels. If a segment legitimately contains the delimiter, pick a different delimiter or rename keys first with the JSON Key Renamer.
Same path written twice (a and a.b)
Last write winsA scalar parent is overwritten by an object when a deeper path arrives later, and an object is overwritten by a scalar the same way. Order-dependent — review colliding keys.
Value looks numeric or boolean ("5432", "true")
Preserved as-isNo type coercion. Strings stay strings. If you need real numbers/booleans for Firestore, cast them in your import code; the JSON Format Fixer repairs syntax but does not cast value types.
File over the free 2 MB limit
Blocked on freeJSON free tier caps files at 2 MB. Upgrade to Pro to remove the file-size limit, or split the document set.
Empty segment from a doubled delimiter (a..b)
Empty key createda..b with delimiter . yields { a: { "": { b: ... } } }. Clean up doubled delimiters in the source before unflattening.
Malformed JSON (trailing comma, single quotes)
Parse errorInput must be valid JSON. Run it through the JSON Format Fixer first to repair quotes, trailing commas, and unquoted keys, then unflatten.
Round-trip from the JSON Flattener with dot array mode
Asymmetric for arraysIf you flattened with JSON Flattener using dot array handling, arrays became numeric segments — unflattening returns objects keyed "0", "1", not the original arrays. Plain object structure round-trips cleanly; arrays do not.
Frequently asked questions
Does the unflattener rebuild Firestore arrays from indexed keys?
No. A flat key like tags.0 becomes the map key "0", producing { tags: { "0": ..., "1": ... } } — a map, not an array. The tool only creates plain objects. Firestore arrays need a real JSON array, so convert those specific fields by hand before calling .set().
Can I unflatten an entire Firestore collection export at once?
Not in a single run. The tool expects one flat JSON object per run, not an array of documents. Process each document's flat object separately and write them in a loop, or use a Cloud Function to batch-write the per-document results.
Does it convert numeric strings like "5432" back to numbers?
No. Values pass through unchanged — "5432" stays a string and "true" stays a string. There is no type inference. Cast types in your import code if Firestore needs real numbers, booleans, or Timestamps.
How do I handle keys with underscores that are part of a field name, not a separator?
Pick a delimiter that does not collide. If you set the delimiter to _, then created_at splits into { created: { at: ... } }, which you may not want. Use dot-notation source keys instead, or rename ambiguous keys first with the JSON Key Renamer.
What separator should I choose for Firestore exports?
Most Firestore/BigQuery exports use dots, so the default . is correct. The Key delimiter field accepts up to 3 characters, so a double-underscore (__) export works too — just type it in before running.
Is the Firestore document data transmitted to JAD Apps?
No. Unflattening runs entirely in your browser. User profiles, subscription data, and any PII in the flat records are never uploaded to JAD Apps servers.
What is the maximum file 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.
Can I control the output indentation?
No. The output is always 2-space indented JSON. If you need minified output for an import payload, run the result through the JSON Minifier.
What happens to GeoPoint, Timestamp, or DocumentReference fields?
Whatever string the export produced for them (an ISO timestamp, a /collection/doc path) is preserved verbatim under the correct nested key. You reconstruct the real Firestore type in your import script — the unflattener only restores the map shape.
How does it handle conflicting keys like a and a.b in the same object?
Last write wins. If a is a scalar and a.b arrives later, a is replaced with { b: ... }. If the order is reversed, the scalar overwrites the object. Reorder or rename the colliding keys to keep the field you need.
Do I need a different tool to flatten Firestore documents first?
If you need to go the other way — nested document to dot-notation keys — use the JSON Flattener. Note that flattening arrays in dot mode yields numeric segments, which this unflattener turns back into objects, not arrays.
My import still fails after unflattening — what should I check?
Confirm array-shaped fields were converted from { "0": ... } maps to real arrays, that timestamps/numbers are cast to their Firestore types in your import code, and that no key over-split on a delimiter inside a field name. Validate the final JSON with the JSON Validator before writing.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.