CSV to TOML Converter
A CSV to TOML converter turns rows into an array of tables that any TOML library deserialises into a list of records. This one decides each column's type once, so a padded product code stays a quoted string instead of becoming an illegal integer, a price keeps its cents, and a date-shaped value only becomes a real date if you ask. Nothing is uploaded.
Need to drop columns or rename headings first? Open the app
00412 is not a number, and TOML agrees
TOML's integer grammar forbids leading zeros. Outside the 0x, 0o and 0b prefixes, a digit sequence starting with a zero is simply not a valid integer, and a parser will reject the line. So a product code of 00412 written bare does not become 412; it stops the whole file from loading.
This is one of the few places where a format's strictness lines up exactly with what you wanted anyway. A padded value is an identifier, not a quantity. Nobody adds two SKUs together. Quoting it is both the only legal option and the correct one.
The decision is made once for the whole column, not cell by cell. A column holding 00412, 907 and 01730 is entirely text, including the middle value, because a column half numbers and half strings is unusable by whatever reads the file. The same rule keeps a price column of 4.25 and 6.80 as text, because 6.80 as a float prints back as 6.8 and the cent is gone.
A worked example
A deployment manifest that started life in a spreadsheet:
name,replicas,cpu_limit,public,region_code,released
billing-api,4,1.5,true,01730,2024-11-03
invoice-worker,2,0.5,false,00412,2024-09-17
Named services, it becomes:
[[services]]
name = "billing-api"
replicas = 4
cpu_limit = 1.5
public = true
region_code = "01730"
released = "2024-11-03"
[[services]]
name = "invoice-worker"
replicas = 2
cpu_limit = 0.5
public = false
region_code = "00412"
released = "2024-09-17"
replicas is an integer, cpu_limit is a float, public is a real boolean, and both region_code and released are strings. Deserialise that in Rust with serde, in Go with BurntSushi, or in Python with tomllib, and you get a list of four correctly typed fields and two strings, which is exactly what the spreadsheet meant.
Dates are a real type, and that is the decision
TOML has four date and time types built into its grammar, with no quotes and no tag: Offset Date-Time, Local Date-Time, Local Date and Local Time. Write released = 2024-11-03 and every conforming reader hands you back a date object, not the string "2024-11-03".
That is a genuine feature and a genuine hazard. If the column really is a date, having the reader parse it saves you a step and catches typos. If the column is something that merely looks like a date, you have silently changed its type. Version strings, some invoice formats and any code with two hyphens in the right places all get caught this way, and the damage shows up downstream when a comparison stops working.
So the default here is to quote them, keeping them strings, and the option to leave them bare is one switch away with a warning either way. When a date-shaped value is quoted you are told that TOML would otherwise have typed it; when it is left bare you are told your reader will hand back a date object.
One detail the option handles for you: TOML's Local Date-Time requires a T between the date and the time. A spreadsheet's 2024-11-03 09:12:00, with a space, is not a valid bare date-time, so when bare dates are enabled the space is replaced.
Empty cells, and the absence of null
TOML has no null. This is deliberate on the format's part and it is the one place where a CSV does not map cleanly onto it. A CSV row with a blank cell means "no value here", and there is no TOML value that says that. key = with nothing after it is a syntax error, and key = "" means the empty string, which is a different claim.
The only honest representation is to omit the key from that row's table entirely. That is what happens here, and the count is reported, because it has a consequence: the tables in the array will not all have the same keys. Deserialising into a Rust struct with a non-Option field, or a Go struct where you check for the zero value, will behave differently for those rows.
If your consumer needs every key present, fill the blanks with a sentinel in the app before converting. Choosing that sentinel is your decision to make, not the converter's.
Keys, escapes and the inline option
A bare TOML key may contain only letters, digits, underscores and hyphens. A heading of Total Revenue (USD) therefore has to be quoted, which is valid and which some libraries surface slightly differently from a bare key when mapping to struct fields. The count of quoted keys is reported so it is not a surprise at deserialisation time.
Values are written as basic strings, and TOML requires every control character in one to be escaped: tabs, newlines and anything below 0x20. A cell holding a genuine newline becomes \n rather than a raw break that would end the line and break the file. Backslashes and double quotes are escaped too.
The inline option writes the whole thing as rows = [ { a = 1, b = "x" }, ... ] instead. It parses to exactly the same data and it is far shorter. It is also unreadable past about five columns, and it loses the property that makes TOML nice to review in a pull request, which is one key per line. Worth it for a small lookup table, not for a manifest anyone has to read.
The TOML to CSV page reads the result back. Its parser is hand-written and pulls in no dependency, which is why that page loads in a few kilobytes rather than shipping thirty to read a forty-line config.
Questions
Why does a table of rows become an array of tables?
Because that is the only construct in TOML that expresses a list of records. Each double-bracketed header, such as [[rows]], starts a new entry, and every TOML library deserialises the result into a list of structs or dicts. There is an inline option that writes the same data as one array of inline tables, which is far more compact and much harder to read past a handful of columns.
Why is my product code quoted when it looks like a number?
Because TOML forbids leading zeros on an integer. 00412 is not a valid TOML integer at all, so writing it bare produces a file that will not parse. Quoting it is not a stylistic choice, it is the only legal way to write it, and it happens to be correct anyway: a padded value is an identifier, not a quantity. Column types are decided once for the whole column, so if any value in it has a leading zero the entire column stays text.
What happens to dates?
They are quoted by default, so they stay strings. TOML types a bare 2024-11-03 as a Local Date and a bare 2024-11-03T09:12:00Z as an Offset Date-Time, which means your reader hands back a date object rather than the text you put in. Sometimes that is what you want and sometimes it silently changes a version string or an invoice code, so it is a switch rather than a guess. Turn it on and date-shaped values are written bare.
What happens to an empty cell?
The key is left out of that row's table. TOML has no null and no undefined; a key with nothing after the equals sign is a syntax error. Omitting the key is the only representation of absence the format has. It does mean rows will not all carry the same keys, which is worth knowing before you deserialise into a struct with non-optional fields, so the count of omitted cells is reported.
Can a column heading with a space in it be a TOML key?
Yes, quoted. TOML's bare key grammar allows only letters, digits, underscores and hyphens, so Total Revenue (USD) has to be written as a quoted key. That is valid TOML and every parser reads it, though some libraries expose a quoted key slightly differently from a bare one when mapping to a struct field. The count of quoted keys is reported so it is not a surprise.
How is TOML different from YAML or JSON for this?
TOML has a smaller, stricter type system and no significant whitespace, which makes it much harder to write an ambiguous file. YAML's 1.1 boolean rules turn an unquoted no into false, which is how Norway's country code disappears from configs; TOML has no such trap. JSON has no comments and no date type. TOML's cost is that it is awkward for deep nesting, which is why a flat table of rows is the shape it handles best.
Is my file uploaded?
No. The conversion runs in your browser tab, with no server round trip, nothing stored between visits, and no row cap. The TOML reader on the return-leg page is hand-written and dependency-free for the same reason: the page stays small and the data stays with you.
Related
Convert your CSV to TOML
No sign-up, no upload, no row cap. An array of tables, typed once per column, with every change reported.
Back to the converter