How to convert csv to json for airtable api imports
- Step 1Export or build your records CSV — Header row = Airtable field names (match them exactly, including spaces and case — Airtable field names are case- and space-sensitive in the API). Each row is one record.
- Step 2Drop the CSV onto the converter above — Accepts
.csv,.tsv,.txt. PapaParse parses in your browser and keeps commas inside quoted cells, so a long-text field with commas stays one field value. - Step 3Choose Array (or NDJSON for big bases) — Array of objects for a base you wrap and POST. NDJSON when you'll slice into 10-record batches — one object per line makes chunking simple.
- Step 4Keep type inference on for Number / Checkbox fields — Leave Infer numbers, booleans, null on so Number fields are numeric and Checkbox fields boolean. Turn it off if a field is Single-line-text holding a numeric-looking code you must keep verbatim.
- Step 5Wrap each record under fields, and the array under records — Map each converted object
{ … }to{ "fields": { … } }, then put them in{ "records": [ … ] }. The cookbook shows the exact transform. This is the shape the create-records endpoint expects. - Step 6POST in batches of 10 and verify — Send up to 10 records per call to
https://api.airtable.com/v0/{baseId}/{table}with your token. Confirm each call returns the created record IDs; the Records stat tells you how many to expect in total.
From converted object to Airtable API body
The tool produces the per-record objects; you wrap them. Field names must match the table exactly.
| Stage | Shape | Note |
|---|---|---|
| This tool's output | [ { "Name": "Ada", "Qty": 3 } ] | Array of objects |
| Per-record wrap | { "fields": { "Name": "Ada", "Qty": 3 } } | Each object nested under fields |
| API body | { "records": [ {fields…}, … ] } | Up to 10 records per call |
Inference and Airtable field types
Inference is global. Off keeps every value as a string.
| CSV cell | JSON (inference on) | Airtable field | Note |
|---|---|---|---|
3 | 3 | Number | Accepts numeric |
true | true | Checkbox | Boolean expected |
null | null | any | Clears the field |
Active | "Active" | Single select | Must match an existing option |
a,b (multi) | "a,b" | Multiple select | Stays a string — API wants ["a","b"]; split first |
00042 (text key) | 42 | Single line text | Leading zero lost — turn inference off |
Cookbook
Recipes for turning a CSV into an Airtable REST API records[] body.
Wrap converted records for the create endpoint
ExampleConvert to an array, map each object under fields, wrap in records. Qty is a number for a Number field.
Input (items.csv):
Name,Qty,InStock
Widget,3,true
Output (array, inference on):
[ { "Name": "Widget", "Qty": 3, "InStock": true } ]
// wrap for Airtable:
{ "records": [ { "fields": { "Name": "Widget", "Qty": 3, "InStock": true } } ] }Chunk into 10-record batches from NDJSON
ExampleAirtable's create endpoint takes max 10 records per call. NDJSON makes 10-line slices easy.
Output (NDJSON): one record per line
for (const chunk of chunksOf(lines, 10)) {
const records = chunk.map(l => ({ fields: JSON.parse(l) }));
await post(`/v0/${baseId}/Items`, { records });
}Keep a text key with leading zeros
ExampleA Single-line-text ID must stay exact. Turn inference off so 00042 is preserved.
Input:
Code,Name
00042,Widget
Output (inference OFF):
[ { "Code": "00042", "Name": "Widget" } ]Omit blank fields on an update
ExampleSkip empty cells so a blank optional field is absent, leaving the existing cell untouched on a PATCH.
Input:
Name,Notes
Widget,
Gadget,Ships Q3
Output (Skip empty cells ON):
[ { "Name": "Widget" }, { "Name": "Gadget", "Notes": "Ships Q3" } ]Split a multi-select column before converting
ExampleMultiple-select fields want a JSON array. Split the column first, then build the array in your wrap step.
Input column Tags: "vip,beta"
// after csv-column-splitter → Tags_1=vip, Tags_2=beta
// then in your wrap code:
fields: { Tags: [row.Tags_1, row.Tags_2].filter(Boolean) }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.
Multiple-select field needs an array, not a string
Not built hereA cell vip,beta for a Multiple-select field becomes one string "vip,beta"; Airtable's API wants ["vip", "beta"]. The converter does not split delimited cells into arrays. Split the column first with csv-column-splitter, or build the array in your wrap code from the split columns.
Single-select option must already exist
API rejectA Single-select field only accepts values that are existing options (unless the field is configured to allow new ones, with typecast). The converter passes the string through faithfully; if Airtable rejects it, the option doesn't exist — add it in the table or send typecast: true in the request.
Text key loses leading zeros
By designA code 00042 destined for a Single-line-text field becomes 42 with inference on. Turn off Infer numbers, booleans, null so it stays a string. Inference is global, so if you also have Number fields, convert with inference off and the API will typecast numerics if you pass typecast: true.
10-records-per-call limit is on your code
Your responsibilityAirtable's create/update endpoints accept at most 10 records per call. The converter produces the full array; chunk it into 10-record batches in your posting code (NDJSON makes this easy). The file size has no relation to the API's per-call cap.
Linked-record / attachment fields not constructed
Not built hereLinked-record fields want an array of record IDs (["rec123"]) and attachments want [{ url }] objects. The converter produces plain values from your cells, not these structures. Build them in your wrap code after looking up the linked record IDs.
Empty cell sends an empty value
Update riskBy default a blank cell becomes "" and would write an empty value. On a create that's harmless; on an update it can clear a cell. Turn on Skip empty cells so blank fields are omitted, leaving existing cell values untouched.
Field name doesn't match the table exactly
API rejectAirtable field names in the API are case- and space-sensitive. A header qty won't map to a field named Qty. The converter uses the header verbatim (trimmed). Match the header text to the field name exactly, or rename with csv-header-rename before converting.
Free tier cap on a large base
LimitFree tier caps at 2 MB / 500 rows. A large base needs Pro (100 MB / 100,000 rows). For very large imports, split the CSV into chunks under the cap, convert each to NDJSON, and POST in 10-record batches.
Frequently asked questions
Does the converter output the records[] / fields shape directly?
No — it outputs a bare array of objects. You wrap each object under fields and the array under records to get { "records": [ { "fields": {…} } ] }. The cookbook shows the exact transform. Keeping the tool envelope-agnostic means the same output also serves other APIs.
Why does Airtable reject my Number field with the value?
If you converted with inference off, the value is a string ("42") and a Number field rejects it. Re-convert with Infer numbers, booleans, null on so it is a real number. Alternatively, send typecast: true in the request so Airtable coerces strings — but typed JSON is cleaner.
How do I respect the 10-records-per-call limit?
Convert to NDJSON and slice it into 10-line chunks in your code, sending one create call per chunk (see the cookbook). The converter does not enforce the limit — it lives in Airtable's API and your posting loop.
My multi-select field needs an array — does the tool make one?
No. A vip,beta cell becomes the string "vip,beta". Split the column first with csv-column-splitter, then build ["vip","beta"] in your wrap code from the split columns. The converter does not turn delimited cells into JSON arrays.
How do I keep a text key with leading zeros?
Turn off type inference so codes like 00042 stay strings for a Single-line-text field. With inference on they become numbers and lose the zeros. If you also have Number fields, convert with inference off and pass typecast: true so Airtable coerces the numerics.
Can it build linked-record or attachment fields?
No. Linked records want an array of record IDs and attachments want [{ url }] objects. The converter produces plain cell values; construct those structures in your wrap code (e.g. after looking up linked record IDs). For non-array fields it gets you the values directly.
Is my base data uploaded during conversion?
No. PapaParse parses and the JSON is built in your browser; base data never reaches a JAD Apps server. Only an anonymous run counter is recorded when signed in, with opt-out available.
How do I avoid clearing cells on an update?
Turn on Skip empty cells so blank fields are omitted from each record. On a PATCH/update, an omitted field leaves the existing cell untouched, whereas an empty string would overwrite it. Use a literal null only when you intend to clear a field.
Why don't my field names map?
Airtable field names are case- and space-sensitive in the API. The converter uses your header text verbatim (trimmed), so qty won't match a field named Qty. Make the headers match the field names exactly, or rename them with csv-header-rename before converting.
How large an import can I convert?
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 bases, split the CSV into chunks under your cap, convert each to NDJSON, and POST in 10-record batches.
Can I validate the records before posting?
Yes — run the wrapped body through json-validator to confirm it is well-formed, or json-tree-viewer to inspect the records[] structure before sending.
Can I automate the import on a schedule?
Yes. Pair the @jadapps/runner once and POST the CSV to 127.0.0.1:9789/v1/tools/csv-to-json/run. A pattern: scheduled export → runner converts to NDJSON → your script wraps and POSTs in 10-record batches. Base data stays on your machine and never reaches JAD's servers.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.