Import CSV or JSON into Firestore: Types, IDs and Dry Runs
Import CSV or JSON into Firestore with explicit field types and document IDs. Follow a Node script with a dry run, compare managed imports and use Chiselbase’s import dialog.
By Chiselbase · Explore the Firestore GUI: Firestore
Import your files with the right format and field types
You have a spreadsheet of products or a JSON dump from another system, and it belongs in a collection. The Google Cloud console import accepts managed-export files from a bucket, not a CSV or plain JSON file. The official gcloud firestore import is meant for the output of a managed export.
So the real question is how to turn rows of text into typed documents without surprises. This guide covers the official command and where it stops, a script that does the job, and the Chiselbase dialog.
Want to review the destination before writing? Chiselbase previews the file and maps its paths into the target collection. For FS-JSON and Chiselbase JSONL, choose insert, overwrite, merge or update, then run the required dry run. See the import workflow.
Way 1: gcloud import, meant for managed exports
If your data already is a managed export, for example from another project, the official command works:
gcloud config set project PROJECT_ID
# Everything in the exportgcloud firestore import gs://BUCKET/2026-09-30T08:15:00_12345 --database="(default)"
# Only some collection groups (the export must have been made with --collection-ids)gcloud firestore import gs://BUCKET/2026-09-30T08:15:00_12345 --database="(default)" \ --collection-ids=productsIt needs the Blaze plan, roles/datastore.importExportAdmin and bucket permissions for both the caller and the job’s Firestore service agent. See the managed import prerequisites. Documents with the same ID are overwritten. Documents that are not in the export stay as they are. You pay one write per imported document. Managed imports do not trigger Cloud Functions; SDK writes can.
For a CSV or a JSON file from anywhere else, there is no official command. Go on to Way 2.
Way 2: a Node script with firebase-admin
The script reads a CSV file row by row, or a JSON file as a whole. It turns each record into a document. CSV cells are always text, so you tell the script which columns are numbers, booleans, timestamps, JSON or references. Without --write it parses the data and reports what it would do; it does not check existing documents or server-side permissions.
Use Node.js 22 or later; this example was checked with firebase-admin 14.5.0 and csv-parse 7.0.3. Bash examples work in Bash or Google Cloud Shell. In PowerShell, use the environment-variable syntax below and put multiline commands on one line. Replace the placeholders and sample export paths. Keep service account keys outside the repository.
npm init -ynpm install firebase-admin csv-parse
# A key for a service account with roles/datastore.userexport GOOGLE_APPLICATION_CREDENTIALS="$HOME/keys/my-project-writer.json"On Windows: $env:GOOGLE_APPLICATION_CREDENTIALS = "C:\keys\my-project-writer.json".
Three input shapes work. A CSV with a header row:
id,name,price,stock,active,createdAt,tagssku_ETH-YIRG-250,Yirgacheffe 250 g,12.5,40,true,2026-09-01T09:00:00Z,"[""light"",""washed""]"sku_GRD-C40,Hand grinder C40,249,6,true,2026-08-14T12:30:00Z,"[""equipment""]"Or a JSON array of objects, or one object keyed by document ID:
[ { "id": "sku_ETH-YIRG-250", "name": "Yirgacheffe 250 g", "price": 12.5, "createdAt": "2026-09-01T09:00:00Z" }]Save this as import-into-firestore.mjs and adjust ID_FIELD and TYPES at the top:
Show import-into-firestore.mjs — complete script
// Usage: node import-into-firestore.mjs <file.csv|file.json> <collectionPath> [--write]// Without --write the script only reports what it would do.import { createReadStream } from "node:fs";import { readFile } from "node:fs/promises";import { parse } from "csv-parse";import { initializeApp, applicationDefault } from "firebase-admin/app";import { getFirestore, Timestamp } from "firebase-admin/firestore";
// Adjust these two to your file.const ID_FIELD = "id"; // column or key that becomes the document ID; missing = generated IDconst TYPES = { // CSV cells are text. List every column that should be something else. price: "number", stock: "number", active: "boolean", createdAt: "timestamp", // ISO 8601, e.g. 2026-09-30T08:15:00Z tags: "json", // e.g. ["decaf","250g"] or {"city":"Porto"} customer: "ref", // a document path, e.g. customers/c_1042};
const [file, collectionPath, flag, ...extra] = process.argv.slice(2);if (!file || !collectionPath || extra.length || (flag && flag !== "--write") || !/\.(csv|json)$/i.test(file)) { console.error("Usage: node import-into-firestore.mjs <file.csv|file.json> <collectionPath> [--write]"); process.exit(1);}const WRITE = flag === "--write";
initializeApp({ credential: applicationDefault() });const db = getFirestore();const collection = db.collection(collectionPath);
function convert(value, type, key) { if (value === undefined || (file.toLowerCase().endsWith(".csv") && value === "")) return undefined; if (value === null) return null; switch (type) { case undefined: return value; case "number": { const n = Number(value); if (!Number.isFinite(n) || (typeof value !== "string" && typeof value !== "number") || String(value).trim() === "") { throw new Error(`${key}: "${value}" is not a finite number`); } return n; } case "boolean": if (typeof value === "boolean") return value; if (/^(true|false)$/i.test(String(value))) return String(value).toLowerCase() === "true"; throw new Error(`${key}: "${value}" is not true or false`); case "timestamp": { const date = new Date(value); if (Number.isNaN(date.getTime())) throw new Error(`${key}: "${value}" is not a date`); return Timestamp.fromDate(date); } case "json": return typeof value === "string" ? JSON.parse(value) : value; case "ref": return db.doc(String(value)); default: throw new Error(`Unknown type "${type}" for ${key}`); }}
// CSV rows arrive one at a time; a JSON file is read whole.async function* records() { if (file.toLowerCase().endsWith(".csv")) { const input = createReadStream(file); const rows = input.pipe( parse({ columns: true, bom: true, skip_empty_lines: true, trim: true }), ); input.on("error", (error) => rows.destroy(error)); try { for await (const row of rows) yield row; } finally { input.destroy(); rows.destroy(); } return; } const parsed = JSON.parse(await readFile(file, "utf8")); if (parsed === null || typeof parsed !== "object") throw new Error("Expected a JSON array or an object keyed by ID"); const list = Array.isArray(parsed) ? parsed // [{ "id": "…", … }, …] : Object.entries(parsed).map(([id, fields]) => { if (!fields || typeof fields !== "object" || Array.isArray(fields)) throw new Error(`${id}: expected a document object`); return { ...fields, [ID_FIELD]: id }; }); // { "<id>": { … } } for (const record of list) yield record;}
const writer = WRITE ? db.bulkWriter() : null;let failed = 0;// A custom handler replaces BulkWriter's default, which retries ABORTED and// UNAVAILABLE up to 10 times. Keep that, and fail every other error at once.const RETRYABLE = new Set([10, 14]); // gRPC status codes ABORTED, UNAVAILABLEwriter?.onWriteError((error) => { if (RETRYABLE.has(error.code) && error.failedAttempts < 10) return true; failed += 1; console.error(`Failed: ${error.documentRef.path}: ${error.message}`); return false;});
let count = 0;try { for await (const record of records()) { if (!record || typeof record !== "object" || Array.isArray(record)) throw new Error("Each record must be an object"); const data = {}; for (const [key, raw] of Object.entries(record)) { if (key === ID_FIELD) continue; const value = convert(raw, TYPES[key], key); if (value !== undefined) data[key] = value; } const id = record[ID_FIELD]; const hasId = id !== undefined && id !== null && id !== ""; if (hasId && (typeof id !== "string" && typeof id !== "number")) throw new Error("Document IDs must be strings or numbers"); if (hasId && (String(id).includes("/") || /^\.{1,2}$|^__.*__$/.test(String(id)) || Buffer.byteLength(String(id)) > 1500)) { throw new Error(`Invalid document ID: ${id}`); } const ref = hasId ? collection.doc(String(id)) : collection.doc(); if (writer) { writer.set(ref, data).catch(() => {}); // set() overwrites a document with the same ID if (++count % 2000 === 0) await writer.flush(); } else { if (count < 3) console.log(ref.path, data); count += 1; } }} finally { if (writer) await writer.close();}if (writer) { console.log(`${count - failed} of ${count} documents written to ${collectionPath}.`); if (failed) process.exitCode = 1;} else { console.log(`Dry run: ${count} documents would be written to ${collectionPath}. Add --write to import.`);}node import-into-firestore.mjs products.csv products # dry run: prints 3 documents and a countnode import-into-firestore.mjs products.csv products --write # the real importWhat to know before you run it with --write:
- Types are yours to decide. A column not listed in
TYPESstays text. That keeps postcodes like01234intact, and it means a forgottenpricecolumn arrives as the string"12.5". - IDs: rows with an
idbecome that document. Rows without one get a generated ID, so running the import twice creates duplicates. - Empty values: empty CSV cells are omitted. JSON
nulland empty strings are kept. CSV headers containing dots are literal field names in this script, not nested paths. Avoid duplicate IDs in an input file. - Overwrites:
set()replaces a document with the same ID. Usewriter.create()instead if existing documents must stay untouched. Those rows then fail and are counted. - Cost: one write per document.
BulkWriterbatches the writes and ramps up traffic slowly, so a large import takes a while on a fresh collection. - Memory: CSV is streamed. A JSON file is read whole, so split very large JSON files first.
- Failures: a failed server write sets a non-zero exit code. This is not an atomic import: if a later row fails parsing or validation, earlier writes can remain. Review the dry run first and check the result of a real run.
- Try it against a running emulator first: set
FIRESTORE_EMULATOR_HOST=127.0.0.1:8080andGCLOUD_PROJECT=demo-import, and unsetGOOGLE_APPLICATION_CREDENTIALS. The Admin SDK uses IAM in the cloud and bypasses Firestore Security Rules.
Way 3: with Chiselbase
Chiselbase imports FS-JSON, its Firestore JSONL format and CSV files into a collection. It does not import a plain JSON array or object, or arbitrary JSONL from another tool. For plain JSON, use the script above. Chiselbase JSONL records carry __path__ and fields, where each field uses the Firestore wire format, such as {"stringValue":"Coffee"}. The export guide explains which format to choose.
- Open the connection, go to Firestore → Data and open the target collection.
- Click Import and pick the file.
- The dialog shows the Source file and a Target collection, pre-filled with the open collection and editable. It previews the file before anything is written.
- For FS-JSON and JSONL, pick a Mode: Insert (new ids only), Overwrite, Merge (keep existing fields) or Update (existing documents only).
- Click Dry run. The report shows what would be written and what would be skipped. Nothing is written.
- Only now is Write N documents enabled, for exactly the file, target and mode you tested. Change one of them and the dialog asks for a new dry run.
The write goes through the connection’s protection level. A write-protected connection stays locked, and production asks you to type the project ID.
For CSV files, a column named __id__ or id becomes the document ID. Chiselbase infers each cell’s type from its content. There is no step in the interface to map columns or set their types. That means a postcode like 01234 is read as the number 1234. If a column must stay text, use the script and leave that column out of TYPES.
CSV imports upsert the supplied fields; the four document modes above apply to FS-JSON and JSONL. The preview and required dry run still apply to CSV.
Import and export is part of Chiselbase Pro; the 14-day trial starts inside the app.
Which way to pick
| Approach | Best fit | Before writing |
|---|---|---|
| Managed import | Restoring a Google managed export | Billing, bucket and IAM required. Matching document IDs are overwritten. |
| Node script | Plain JSON or CSV with explicit column types | Local parsing dry run. The example replaces matching IDs; you maintain conversion and error handling. |
| Chiselbase | FS-JSON, Chiselbase JSONL or a CSV you want to preview | File preview, target path mapping and required dry run. Four modes for FS-JSON/JSONL; CSV infers types and upserts. Pro or trial required. |
Moving a managed export between projects: gcloud. A spreadsheet with columns that need exact types: the script. Files exported by Chiselbase, with target mapping and a required dry run on the way in: Chiselbase.
Limits and questions
Can I import nested data from CSV?
A CSV cell holds text. In the script, declare the column as json and write the value as JSON, like {"city":"Porto"}. Deeper structures are easier as a JSON file.
Why did my numbers arrive as strings?
The column was not listed in TYPES, so the script kept it as text. Add it and run the import again with the same IDs. set() replaces the documents.
How many documents can I import at once?
The example has no document-count cap, but JSON input must fit in memory. Each document must also fit Firestore’s document and field limits. Firestore bills successful writes and recommends ramping up traffic on new collections; BulkWriter handles that ramp-up.
Can I undo an import?
Firestore itself has no undo. Test on a scratch collection or the emulator first. For a safety net on live data, turn on point-in-time recovery before the import. See how to back up Firestore and restore it.
For files you export and re-import while working in Firestore, download Chiselbase and try Pro for 14 days in the app. The file preview, destination mapping and required dry run stay next to the collection you are editing.
Sources and verification
Checked on 4 October 2026 against Google’s managed import documentation, Admin SDK setup and the current Chiselbase implementation. The Node example was exercised against a local emulator, including CSV and JSON input; cloud commands were checked against the CLI documentation.
Next: how to export Firestore to JSON, CSV or JSONL and how to copy Firestore data between projects. More on working with Firestore data is on the Firestore page.