﻿Source: https://chiselbase.com/guides/import-csv-json-into-firestore/

Published 2026-09-30 · Updated 2026-10-04

# 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](https://chiselbase.com/about/)  · Explore the Firestore GUI: [Firestore](https://chiselbase.com/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](https://chiselbase.com/guides/import-csv-json-into-firestore/#way-3-with-chiselbase).

## 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:

Terminal window

```bash
gcloud config set project PROJECT_ID

# Everything in the export
gcloud 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=products
```

It 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](https://firebase.google.com/docs/firestore/manage-data/export-import#before_you_begin). 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.

Terminal window

```bash
npm init -y
npm install firebase-admin csv-parse

# A key for a service account with roles/datastore.user
export 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:

```csv
id,name,price,stock,active,createdAt,tags
sku_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:

```json
[
  { "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

import-into-firestore.mjs

```js
// 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 ID
const 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, UNAVAILABLE
writer?.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.`);
}
```

Terminal window

```bash
node import-into-firestore.mjs products.csv products           # dry run: prints 3 documents and a count
node import-into-firestore.mjs products.csv products --write   # the real import
```

What to know before you run it with `--write`:

-   **Types are yours to decide.** A column not listed in `TYPES` stays text. That keeps postcodes like `01234` intact, and it means a forgotten `price` column arrives as the string `"12.5"`.
-   **IDs**: rows with an `id` become that document. Rows without one get a generated ID, so running the import twice creates duplicates.
-   **Empty values**: empty CSV cells are omitted. JSON `null` and 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. Use `writer.create()` instead if existing documents must stay untouched. Those rows then fail and are counted.
-   **Cost**: one write per document. `BulkWriter` batches 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:8080` and `GCLOUD_PROJECT=demo-import`, and unset `GOOGLE_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](https://chiselbase.com/guides/export-firestore-to-json/) explains which format to choose.

1.  Open the connection, go to **Firestore → Data** and open the target collection.
2.  Click **Import** and pick the file.
3.  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.
4.  For FS-JSON and JSONL, pick a **Mode**: **Insert (new ids only)**, **Overwrite**, **Merge (keep existing fields)** or **Update (existing documents only)**.
5.  Click **Dry run**. The report shows what would be written and what would be skipped. Nothing is written.
6.  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](https://firebase.google.com/docs/firestore/quotas). 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](https://chiselbase.com/guides/back-up-firestore-and-restore/).

For files you export and re-import while working in Firestore, [download Chiselbase](https://chiselbase.com/download/) 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](https://firebase.google.com/docs/firestore/manage-data/export-import), [Admin SDK setup](https://firebase.google.com/docs/admin/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](https://chiselbase.com/guides/export-firestore-to-json/) and [how to copy Firestore data between projects](https://chiselbase.com/guides/copy-firestore-data-between-projects/). More on working with Firestore data is on the [Firestore page](https://chiselbase.com/firestore/).

## Keep working with Firestore

[All guides](https://chiselbase.com/guides/)

-   [Find missing fields and mixed types Audit your schema and explain why queries leave records out.](https://chiselbase.com/guides/firestore-missing-fields-and-schema/)
-   [Review a bulk update Preview field changes, handle conflicts and verify the result.](https://chiselbase.com/guides/bulk-update-firestore-documents/)
-   [Export Firestore data Choose JSON, CSV or a format that preserves Firestore types.](https://chiselbase.com/guides/export-firestore-to-json/)

Chiselbase

### Work with your data in one place.

Browse collections, preview imports and review migrations in a desktop Firestore GUI.

[Download Chiselbase](https://chiselbase.com/download/)

[Explore the Firestore GUI](https://chiselbase.com/firestore/) [See Free and Pro](https://chiselbase.com/pricing/)
