Quick answer: Read the CSV with a real parser (
csv.DictReaderin Python,csv-parsein Node, PapaParse in the browser), get one object per row, and serialize the list withjson.dumporJSON.stringify. Every value starts life as a string; convert only the columns you know are numeric or boolean. Automatic type inference is what turns zip code02134into2134and the product code1e5into100000.
A customer import ran clean, 40,000 rows, no errors. Two weeks later support noticed that every Boston address had a four-digit zip code. The converter had seen a column full of digits and decided it was a number. Nothing crashed, which is why it took two weeks. CSV to JSON is easy to do and easy to do slightly wrong, so this guide starts with the trap and then shows five ways to avoid it.
Try the CSV to JSON Converter on a non-sensitive sample, then inspect the resulting objects in the JSON Viewer.
The Trap: CSV Has No Types, So Tools Guess
A CSV cell is just characters between delimiters. 02134, 1e5, null, and TRUE are all text until something decides otherwise. JSON, on the other hand, distinguishes strings, numbers, booleans, and null, so every converter has to choose a type for each cell. Tools that choose automatically get the common case right and the edge cases silently wrong:
| Cell text | Inference produces | You probably wanted | Where it bites |
|---|---|---|---|
02134 | 2134 (number) | "02134" (string) | Zip and postal codes, account numbers |
1e5 | 100000.0 (float) | "1e5" (string) | Product codes, batch IDs |
null | null (JSON null) | "null" or null, your call | Free-text fields where someone typed the word |
TRUE | true (boolean) | true, usually | Flags; but check the column is really boolean |
12345678901234567890 | 1.2345678901234567e19 | "12345678901234567890" | Snowflake IDs, card tokens |
(empty) | NaN or null | null, or omit, or "" | Optional fields; decide once per column |
2025-01-05 | Timestamp object | "2025-01-05" (string) | Dates: JSON has no date type |
The large-integer row deserves a second look. JSON itself can hold 12345678901234567890exactly, but a JavaScript consumer will round it the moment it calls JSON.parse. The JSON number precision guide explains why IDs above 253 belong in strings.
The rule that follows from all this: treat every column as a string unless you explicitly decide otherwise. Each method below shows how to make that decision per column.
Method 1: Python csv.DictReader + json (No Dependencies)
The standard library gives you strings for everything, which is the safe starting point. Add a small mapping of column name to cast function for the fields you want typed:
import csv
import json
def to_int(s):
return int(s) if s.strip() else None
def to_float(s):
return float(s) if s.strip() else None
def to_bool(s):
return s.strip().lower() in ("true", "1", "yes")
CASTS = {
"id": to_int,
"qty": to_int,
"price": to_float,
"active": to_bool,
# zip, phone, sku: not listed, so they stay strings
}
rows = []
with open("customers.csv", newline="", encoding="utf-8-sig") as f:
for row in csv.DictReader(f):
rows.append({col: CASTS.get(col, str)(val) for col, val in row.items()})
with open("customers.json", "w", encoding="utf-8") as f:
json.dump(rows, f, indent=2, ensure_ascii=False)
Three details carry weight. newline="" lets the csv module handle line endings inside quoted fields. utf-8-sig strips a byte order mark if the file came from Excel; without it your first key becomes id and nothing matches. And ensure_ascii=False keeps accented names readable instead of escaping them as é.
The output for a two-row file looks like this:
[
{
"id": 1,
"name": "Priya Nair",
"zip": "02134",
"qty": 3,
"price": 19.99,
"active": true
},
{
"id": 2,
"name": "Jonas Weber",
"zip": "10115",
"qty": null,
"price": 4.5,
"active": false
}
]
Method 2: pandas (Control the dtype)
pd.read_csv infers types aggressively by default, and it also treats the strings NA, N/A, null, and a dozen others as missing values. Both behaviours are overridable:
import pandas as pd
df = pd.read_csv(
"customers.csv",
dtype={"zip": str, "phone": str, "sku": str, "id": "Int64"},
keep_default_na=False, # the word "null" stays the word "null"
na_values=[""], # only truly empty cells become missing
encoding="utf-8-sig",
)
df.to_json("customers.json", orient="records", indent=2, force_ascii=False)
orient="records" is the one that produces an array of objects; the default orient produces a column-oriented object that surprises everyone the first time. Missing values serialize as null. If you need an integer column that can also be missing, use the nullable "Int64" dtype as shown, otherwise pandas upcasts the whole column to float and 3 becomes 3.0.
One caution: to_json writes dates as epoch milliseconds by default. Pass date_format="iso" if any column was parsed as a datetime, or keep dates as strings in the first place. ISO 8601 strings are the convention almost every consumer expects.
Method 3: Node.js With csv-parse
csv-parse (from the csv package family) handles quoting, multi-line fields, and BOMs. Its cast option receives the column name, so per-column rules are straightforward:
import fs from "node:fs";
import { parse } from "csv-parse/sync";
const STRING_COLUMNS = new Set(["zip", "phone", "sku"]);
const records = parse(fs.readFileSync("customers.csv", "utf8"), {
columns: true, // first row becomes keys
bom: true, // strip a leading BOM
skip_empty_lines: true,
trim: true,
cast: (value, context) => {
if (context.header) return value;
if (STRING_COLUMNS.has(context.column)) return value;
if (value === "") return null;
if (value === "true" || value === "false") return value === "true";
if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
return value;
},
});
fs.writeFileSync("customers.json", JSON.stringify(records, null, 2));
The regular expression is deliberately strict: it matches plain decimals and nothing else, so 1e5 stays a string. That is the opposite of Number("1e5"), which happily returns 100000. For files too large to hold in memory, swap the sync import for the streaming API and pipe records to a JSON Lines file one per line.
Method 4: PapaParse in the Browser
When the CSV comes from a user's computer and should never touch your server, PapaParse parses it client-side. The dynamicTyping option accepts an object, which is how you opt in column by column instead of globally:
const input = document.querySelector("#csv-file");
input.addEventListener("change", () => {
Papa.parse(input.files[0], {
header: true,
skipEmptyLines: true,
dynamicTyping: { qty: true, price: true, active: true }, // only these
complete: (results) => {
const json = JSON.stringify(results.data, null, 2);
console.log(json);
if (results.errors.length) console.warn(results.errors);
},
});
});
PapaParse also detects the delimiter, so a semicolon-separated export from a European Excel works without configuration. Check results.errors; a row with the wrong number of fields is reported there rather than thrown.
Method 5: jq for Simple Files
jq has no CSV parser, but for a file with no quoted commas you can split lines yourself. Read raw with -R, slurp with -s, and zip each row against the header:
jq -R -s '
split("\n") | map(select(length > 0)) | map(split(","))
| .[0] as $h
| .[1:] | map([$h, .] | transpose | map({(.[0]): .[1]}) | add)
' customers.csv
Everything comes out as strings, which for a quick look is fine. Add | tonumber on specific fields if you need it. The moment a field contains a quoted comma, this breaks; switch to one of the real parsers above. More jq idioms are collected in the jq cheat sheet.
Array of Objects vs Object Keyed by ID
Every method above produces an array of objects, one per row. That is the right default: it preserves row order, it allows duplicate IDs, and it is what REST APIs, pandas, and every CSV tool expect when going the other way. Sometimes, though, the consumer wants to look rows up by key without scanning:
// JavaScript
const byId = Object.fromEntries(records.map((r) => [r.id, r]));
# Python
by_id = {row["id"]: row for row in rows}
Both silently keep the last row when an ID repeats, so check uniqueness first if that matters. The JSON array vs object guide goes deeper on when each shape serves consumers better.
Validate the Output
A converter that writes JSON by string concatenation can produce output that looks right and fails to parse: a trailing comma after the last row, an unescaped quote in a name, a stray BOM. Every example here uses a real serializer, which prevents that class of bug, but it is still worth confirming the file is what you think it is.
Paste the result into JSON Prettifier. Validation gives you a line and column for any syntax problem, and the formatted view makes the type question visible at a glance: a zip code shown in the string colour with quotes around it is right, one shown as a bare number is the bug from the opening paragraph. Sort Keys lines up every object the same way so you can scan down a column and spot a null where a string should be. Files up to 5 MB can be uploaded directly.
Common Mistakes
- Splitting on commas yourself.
line.split(",")breaks on the first address with a comma in it. Always use a parser that understands quoting. - Ignoring the BOM. Excel-exported CSV often starts with
. Your first key becomes"id"androw.idis undefined. Useutf-8-sigin Python orbom: truein csv-parse. - Global dynamic typing. Turning on inference for every column is how zip codes, phone numbers, and product codes get mangled. Opt in per column.
- Treating the word null as missing. If a survey answer is literally the string
null, pandas drops it by default. Setkeep_default_na=Falseand name the markers you actually use. The null vs missing guide helps you choose what missing should look like in the output. - Trailing empty line. Most files end with a newline. Without
skip_empty_linesorskipEmptyLinesyou get a final object of empty strings. - Duplicate header names. Two columns called
namecollapse into one key. Rename them before parsing, or csv-parse'scolumnsoption can take a function that returns unique names.
Going the Other Direction
If the JSON you just produced has to go back to a spreadsheet later, keep the nesting flat and the arrays out of cells. The JSON to CSV guide covers the reverse trip, including how to flatten nested objects and the UTF-8 BOM Excel needs to show accents correctly.
If a spreadsheet-facing export has protective prefixes, test that return journey explicitly with the JSON-to-CSV export safety checklist. A display-oriented representation can intentionally differ from the original JSON values.