How to convert csv to json for firestore bulk import
- Step 1Export your collection data as CSV — Save a CSV with a header row. Headers become Firestore field names, so name them as your data model expects (camelCase fields, an
idcolumn if you want explicit document IDs). - Step 2Drop the CSV onto the converter above — Accepts
.csv,.tsv,.txt. PapaParse auto-detects the delimiter and keeps commas/newlines inside quoted cells intact, so a multi-line description stays one field value. - Step 3Choose Array of objects (or NDJSON for big seeds) — Array of objects for a
require()-and-loop seed. NDJSON when you want to stream a large file and batch writes as you go to respect Firestore's 500-per-batch limit. - Step 4Decide inference for your ID column — Keep inference on so data fields (
age,price,isActive) store as the right Firestore types. Turn it off if your ID column has leading zeros or is a big number you will use as the document ID string. - Step 5Consider Skip empty cells for optional fields — By default a blank cell stores as an empty string. Turn on Skip empty cells to omit the field so Firestore treats it as absent — usually what you want for optional fields and to keep documents lean.
- Step 6Download and run your seed script — Download JSON, then in your Admin SDK script:
const data = require('./data.json'); for (const r of data) await db.collection('users').add(r);. Use the ID field as.doc(r.id).set(r)if you kept IDs as strings.
Inference → Firestore field type
How an inferred JSON value stores in Firestore. Inference is global; off means every field is a string.
| CSV cell | JSON (inference on) | Firestore type | Note |
|---|---|---|---|
36 | 36 | integer | Numeric range queries work |
3.14 | 3.14 | double | Decimal |
true | true | boolean | Security rules is bool pass |
null | null | null | Distinct from missing field |
007 (doc id) | 7 | number | Leading zero lost — turn inference off for IDs |
9007199254740993 | "9007…" | string | Beyond safe int — kept as string for use as doc ID |
2026-06-12 | "2026-06-12" | string | Not a Timestamp — convert in your seed if needed |
Output mode → seed approach
The tool produces the file; your Admin SDK script does the writes.
| Output mode | Seed approach | Watch |
|---|---|---|
| Array of objects | require() → loop add() / set() | Whole array in memory |
| NDJSON | stream lines → batch writes | Chunk to ≤500 writes per WriteBatch (Firestore limit) |
| Grouped by column | { key: [ … ] } | Not a record set — only for building a single map document |
Cookbook
Conversion recipes aimed at a Firestore Admin SDK seed.
Typed array for a seed loop
ExampleDefault settings. age stores as integer and active as boolean so range queries and rules work.
Input (users.csv):
name,age,active
Ada,36,true
Linus,54,false
Output (array, inference on):
[
{ "name": "Ada", "age": 36, "active": true },
{ "name": "Linus", "age": 54, "active": false }
]
// seed.js
const data = require('./users.json');
for (const r of data) await db.collection('users').add(r);Use an ID column as the document ID
ExampleKeep IDs as strings (inference off) so you can pass them to .doc(id). Avoids zero-stripping a key like 00042.
Input:
id,name
00042,Ada
Output (inference OFF):
[ { "id": "00042", "name": "Ada" } ]
// seed with explicit doc id
for (const r of data) await db.collection('users').doc(r.id).set(r);Omit optional fields with Skip empty cells
ExampleA blank nickname omits the field so the document is lean and the field reads as absent, not empty string.
Input:
name,nickname
Ada,
Linus,Tux
Output (Skip empty cells ON):
[ { "name": "Ada" }, { "name": "Linus", "nickname": "Tux" } ]Batch writes from NDJSON for a big seed
ExampleStream NDJSON and commit in chunks of 500 to stay under Firestore's WriteBatch limit.
Output (NDJSON): one {record} per line
// pseudo-seed
let batch = db.batch(); let n = 0;
for (const line of readLines('data.ndjson')) {
batch.set(db.collection('users').doc(), JSON.parse(line));
if (++n % 500 === 0) { await batch.commit(); batch = db.batch(); }
}
await batch.commit();Convert a date string to a Timestamp in the seed
ExampleDates stay strings here; cast them in your script since the converter does not produce Firestore Timestamps.
Output element:
{ "name": "Ada", "joined": "2026-06-12" }
// in the seed, before writing:
r.joined = admin.firestore.Timestamp.fromDate(new Date(r.joined));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.
Document ID loses leading zeros
By designWith inference on, an ID like 00042 becomes 42. If that column is your intended document ID (.doc(id)), turn off Infer numbers, booleans, null so it stays the exact string "00042". Inference is global, so if you also need typed data fields, keep inference off here and cast the few numeric fields in your seed script.
Date string is not stored as a Firestore Timestamp
Expected2026-06-12 stays a JSON string, so Firestore stores a string field, not a Timestamp. Convert in your seed: admin.firestore.Timestamp.fromDate(new Date(r.joined)) before writing. The converter has no Timestamp type because JSON has none.
Nested map field not built from dotted headers
Not supported hereA header address.city becomes a flat field literally named address.city, not a Firestore map { address: { city } }. To store a nested map, convert here, then run the array through json-unflattener to expand dotted keys into nested objects, which the Admin SDK stores as maps.
Empty cell stored as empty string, not absent
ExpectedBy default a blank cell becomes "", so the document has an empty-string field. For Firestore, an absent field and an empty string behave differently in queries and rules. Turn on Skip empty cells to omit the field so it reads as absent.
Big numeric ID kept as string
PreservedAn ID beyond JavaScript's safe-integer range is kept as a string by inference, so it is not corrupted to a rounded number. This is ideal if you use it as a document ID (which must be a string anyway). If you need it numeric in a field, store it as a string and parse server-side, accepting Firestore's own number precision rules.
Batch write limit is on your script, not the file
Your responsibilityThe converter happily produces an array of any size up to your tier cap, but Firestore's WriteBatch commits at most 500 writes. That limit lives in your seed script — chunk the array into 500-write batches (see the NDJSON example). The file shape itself imposes no such limit.
Duplicate header collapses a field
OverwriteTwo columns with the same header map to one field; the later value wins. Rename one column before converting so both values land in distinct Firestore fields.
Free tier file/row cap on a large collection
LimitFree tier caps at 2 MB / 500 rows. A real collection seed usually needs Pro (100 MB / 100,000 rows) or Developer (5 GB). For very large seeds, split the CSV into chunks under the cap, convert each to NDJSON, and stream-batch each file.
Frequently asked questions
Why convert to JSON when Firestore has no CSV import anyway?
Because the standard import path is an Admin SDK script that reads a JSON array and writes documents. You need a clean, typed array as the script's input. This tool produces it, with inference so numbers and booleans store as Firestore's integer/double/boolean types rather than strings that break queries and rules.
How do I keep an ID column usable as a document ID?
Turn off type inference so the ID stays a string (Firestore document IDs must be strings). Then in your seed use db.collection('x').doc(r.id).set(r). With inference on, a leading-zero ID like 00042 would become the number 42 and lose its zeros.
Will date columns become Firestore Timestamps?
No — dates stay JSON strings, so Firestore stores string fields. Convert in your seed script with admin.firestore.Timestamp.fromDate(new Date(r.field)) before writing. JSON has no native timestamp type, so the converter cannot emit one.
Can it create nested map fields from my columns?
Not from dotted headers directly — address.city becomes a flat field of that literal name. To get a Firestore map, convert here, then run the records through json-unflattener to expand dotted keys into nested objects, which the Admin SDK stores as maps.
How do I handle Firestore's 500-write batch limit?
That limit is enforced in your seed script, not the file. Convert to NDJSON, stream it, and commit a WriteBatch every 500 writes (see the cookbook example). The converter produces the records; your script controls batching.
Should I use empty string or omit the field for optional data?
Usually omit. Turn on Skip empty cells so a blank cell drops the field, and Firestore treats it as absent — cleaner documents and correct behaviour for where(field, '==', null) style logic. Leave it off only if you specifically want empty-string fields.
Is my collection data uploaded during conversion?
No. PapaParse parses and the JSON is built in your browser; the data never reaches a JAD Apps server. Only an anonymous run counter is recorded when signed in, and you can opt out.
How large a CSV can I convert for a seed?
Free: 2 MB / 500 rows. Pro: 100 MB / 100,000 rows. Pro+Media: 500 MB / 500,000 rows. Developer: 5 GB, no row cap. For large seeds, split into chunks under your cap, convert each to NDJSON, and stream-batch them.
What about big numeric IDs — do they get corrupted?
No. Inference keeps integers beyond JavaScript's safe range as strings, so the value is exact. That is convenient because Firestore document IDs are strings anyway. For a numeric field, store the string and parse it server-side.
Can I generate a TypeScript type or a JSON Schema for the documents?
Yes, with siblings: run the converted array through json-to-typescript for an interface to type your Firestore reads, or json-schema-generator for a JSON Schema to validate the data before seeding.
Can I automate the conversion before running a seed in CI?
Yes. Pair the @jadapps/runner once and POST the CSV to 127.0.0.1:9789/v1/tools/csv-to-json/run. A typical step: export → runner converts → Admin SDK seed against an emulator or staging project. Data stays on your machine and never reaches JAD's servers.
Should I use array or NDJSON for a Firestore seed?
Use Array for a small or medium seed you require() and loop over. Use NDJSON for a large collection so you can stream the file line by line and commit a WriteBatch every 500 records — that keeps memory low and respects Firestore's batch limit. Both produce the same documents; the choice is about how your seed script reads them.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.