Skip to content
How-To

How to Convert CSV to JSON (Without Breaking Your Numbers)

·8 min read
How to Convert CSV to JSON (Without Breaking Your Numbers) - How-To guide from JSON Prettifier

Quick answer: Read the CSV with a real parser (csv.DictReader in Python, csv-parse in Node, PapaParse in the browser), get one object per row, and serialize the list with json.dump or JSON.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 code 02134 into 2134 and the product code 1e5 into 100000.

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 textInference producesYou probably wantedWhere it bites
021342134 (number)"02134" (string)Zip and postal codes, account numbers
1e5100000.0 (float)"1e5" (string)Product codes, batch IDs
nullnull (JSON null)"null" or null, your callFree-text fields where someone typed the word
TRUEtrue (boolean)true, usuallyFlags; but check the column is really boolean
123456789012345678901.2345678901234567e19"12345678901234567890"Snowflake IDs, card tokens
(empty)NaN or nullnull, or omit, or ""Optional fields; decide once per column
2025-01-05Timestamp 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" and row.id is undefined. Use utf-8-sig in Python or bom: true in 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. Set keep_default_na=False and 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_lines or skipEmptyLines you get a final object of empty strings.
  • Duplicate header names. Two columns called name collapse into one key. Rename them before parsing, or csv-parse's columns option 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.

Frequently Asked Questions

What is the easiest way to convert CSV to JSON?
In Python, csv.DictReader plus json.dump does it in five lines with no dependencies and treats every value as a string, which is the safe default. If you want numbers and booleans typed, cast specific columns explicitly rather than relying on automatic inference.
Why do my zip codes lose their leading zeros when converting CSV to JSON?
Because the converter inferred the column as numeric and 02134 is the number 2134. Force the column to string: dtype={'zip': str} in pandas, a cast function that returns the raw value in csv-parse, or simply skip dynamic typing for that column in PapaParse.
Should CSV become a JSON array of objects or an object keyed by ID?
An array of objects is the standard output and preserves row order, which is what most APIs and tools expect. Convert to an object keyed by ID only when consumers will look rows up by that key and the key is unique.
How do I convert CSV to JSON in the browser without uploading the file?
Use PapaParse with a File object from an input element; parsing happens entirely in the user's browser. Set header: true to get one object per row and keep dynamicTyping off, or enable it only for columns you know are numeric.
How do I handle empty cells in CSV when converting to JSON?
Decide per column. For optional numbers and dates, emit null; for free-text fields, an empty string is often more honest than null. Whatever you pick, apply it consistently so consumers can rely on it.
Can jq convert CSV to JSON?
For simple CSV without quoted commas, yes: read with -R, split lines on commas, and zip with the header row. jq has no real CSV parser, so for files with quoted fields use Python or csv-parse instead.

Check the JSON You Just Made

Paste the converted output, validate it, and see at a glance which values came through as strings and which as numbers. Nothing leaves your browser.

Validate My JSON