T tomlkit·org
Inspect Formatter Validator Lint Stats Keys Query Convert TOMLJSON JSONTOML TOMLYAML YAMLTOML INITOML TOMLINI .envTOML TOML.env TOMLTS TOMLCSV CSVTOML TOMLXML TOML.properties .propertiesTOML Transform Sort keys Flatten Unflatten Redact Minify Generate Go struct Rust struct Python types JSON Schema Compare Diff Merge

CSV to TOML

updated 20 August 2026

Paste a sheet, get a config. Each CSV row becomes one [[rows]] entry with the header as key names and numbers, booleans, and text detected for you. A two-column key,value sheet can instead become a flat — or re-nested — config document.

delimiter auto-detected

What this tool does

It parses CSV — quoted fields, embedded commas, doubled quotes, CRLF, a UTF-8 BOM — and writes TOML. In the default Array of tables layout, the header row supplies key names and every data row becomes one [[rows]] block. In Key / value layout it reads the first two columns as key and value and emits a single flat table, optionally re-nesting dotted keys into real sections.

The intent it closes: "this data lives in a spreadsheet and needs to be a config file." Lists of servers, feature flags, rate limits, locales, redirect rules — all of it is easier to maintain in a sheet and easier to ship as TOML.

Values are typed, not just copied. 8 becomes an integer, 0.75 a float, true and false (and yes/no) become booleans, and everything else stays a quoted string. Numbers too large to represent exactly are kept as strings rather than silently rounded.

When you'd reach for it

  • Ship a spreadsheet as config. Someone maintains the list in Sheets; your service reads TOML.
  • Seed a config from a query result. Export the query to CSV, convert, paste into the file.
  • Round-trip a bulk edit. Export with TOML to CSV, edit in a spreadsheet, convert back here.
  • Build fixtures for tests. A table of cases becomes an array of tables your test harness can iterate.
  • Turn a two-column mapping into a config. A key,value sheet with dotted keys re-nests into real tables.

How it works

Four steps, and the types are decided in the third.

1. Detect the delimiter

On Auto-detect, the first line is scanned for commas, semicolons, tabs, and pipes, and whichever appears most often wins. Set it explicitly when the header line is unusual — a single-column file, or one where a quoted field contains more semicolons than the real delimiter.

2. Parse the rows

A character-by-character parse handles the awkward parts of CSV properly: fields wrapped in double quotes, delimiters and newlines inside those quotes, doubled quotes as an escaped quote, stray carriage returns, and a leading byte-order mark. Blank lines are skipped.

3. Name the keys and type the values

Header cells become key names — verbatim, or converted to snake_case if you pick that. Duplicate headers get a numeric suffix so no column is lost, and an empty header cell becomes colN. Then each cell is typed: integers, floats, and booleans are recognised; anything else stays a string.

4. Emit TOML

In array-of-tables layout, the rows are written under the table name you chose, one [[name]] block per row. In key/value layout, the pairs become a single table, and with dotted re-nesting on, server.port becomes a port key inside [server].

Options reference

Layout

Array of tables treats the CSV as data: one [[table]] block per row. Key / value config treats the first two columns as a settings list and emits one table of keys — the shape to use for a sheet that was exported from a config in the first place.

Table name

The name used for the generated [[blocks]]. Use whatever the consuming code expects — servers, routes, flags. Defaults to rows.

Header row / Key style

Turn the header off for a file that starts straight into data; columns are then named col1, col2, and so on. snake_case is worth setting when the header holds human labels — "Max Retries" becomes max_retries, which is idiomatic TOML and legal without quoting.

Types

With inference on, 8 is an integer, 0.75 a float, true/false/yes/no booleans, and everything else a string. Turn it off when a column of digits is really an identifier — a zip code or an account number that must keep its leading zeros.

Empty cells

Keep as empty string writes key = "", so every entry has the same shape. Omit the key leaves it out entirely, which is what you want when the consuming code checks for presence and has its own defaults.

Example

Input:

name,ip,cores,active
alpha,10.0.0.1,8,true
beta,10.0.0.2,16,false

Output, with the table named servers:

[[servers]]
name = "alpha"
ip = "10.0.0.1"
cores = 8
active = true

[[servers]]
name = "beta"
ip = "10.0.0.2"
cores = 16
active = false

cores came through as an integer and active as a boolean because inference is on; ip stayed a string because 10.0.0.1 is not a number. Turn inference off and all four fields would be quoted strings.

Limits and notes

  • One table per conversion. A CSV is one table, so the output is one array of tables. Convert twice and paste if a document needs two.
  • No nested arrays from a cell. A cell holding ["a","b"] becomes that literal string, not a TOML array. Split it into columns if the structure matters.
  • Type inference is per cell, not per column. A column that is mostly numbers but has one n/a yields a mix of integers and a string — valid TOML, but check whether the consumer expects one type.
  • Very large sheets are memory-bound. Everything is parsed in the tab. Tens of thousands of rows are fine; a hundred-megabyte export belongs in a script.

FAQ

Does it handle quoted fields with commas inside?

Yes. The parser follows RFC 4180: fields may be wrapped in double quotes, those fields may contain the delimiter and newlines, and a doubled quote inside them means a literal quote. A UTF-8 BOM is stripped, and CRLF endings are handled.

My numeric IDs lost their leading zeros. How do I stop that?

Set Types to Everything is a string. Inference turns 007 into the integer 7, which is correct arithmetic and the wrong answer for an identifier.

What if two columns have the same header?

The second gets a numeric suffix — name and name_2 — so no column is silently dropped. Rename them in the sheet if the suffix is not meaningful.

Can I get nested tables instead of a flat array?

In key/value layout, yes: dotted keys re-nest, so a row of server.port,8080 lands as port = 8080 under [server]. In array-of-tables layout, dotted header names become nested keys within each entry.

Is the file uploaded?

No. Parsing and emission both happen in your browser — the sheet never leaves the tab, which matters when the export came out of an internal system.