I had a 50MB CSV of bank statements that I needed in JSON format for a dashboard prototype. Every online converter I tried either capped the file size at 5MB or required creating an account. I was not uploading financial records to a random server anyway. Ten lines of PapaParse ran the whole conversion locally in under 3 seconds.
Need the conversion done right now?
Our CSV to JSON tool runs entirely in your browser. Your file never leaves your device.
Open CSV to JSON Converter βWhy Uploading Your CSV Is a Bad Idea
When you use a server-side converter, your file travels over HTTP to someone's server, gets parsed in memory there, and the output comes back. The company's logs almost certainly record the upload. Their infrastructure team can access the files. For CSV data containing customer records, financial data, or anything personally identifiable, that is a real compliance problem under GDPR, HIPAA, and most corporate data policies.
Browser-based parsing solves this by running the entire conversion inside the V8 JavaScript engine on your own device. The file never touches a network request. Nothing is stored. When the tab closes, the data is gone.
PapaParse: The Right Tool for This Job
PapaParse is the standard for browser-side CSV parsing. It handles quoted fields, embedded commas, escaped quotes, multi-line cells, custom delimiters, and BOM-prefixed UTF-8 files out of the box. Writing your own CSV parser from scratch will hit edge cases within the first real-world file you test. PapaParse has already hit them and fixed them.
Basic CSV to JSON Conversion
Here is the minimal working implementation for a file input element:
import Papa from 'papaparse';
// Wire up to a file input: <input type="file" id="csvFile" accept=".csv" />
const input = document.getElementById('csvFile') as HTMLInputElement;
input.addEventListener('change', (event) => {
const file = (event.target as HTMLInputElement).files?.[0];
if (!file) return;
Papa.parse(file, {
header: true, // Use first row as JSON keys
skipEmptyLines: true, // Ignore blank rows
dynamicTyping: true, // Convert "42" to 42, "true" to true automatically
complete: (results) => {
const json = JSON.stringify(results.data, null, 2);
console.log('Rows parsed:', results.data.length);
console.log('Output:', json);
// Download the result as a .json file
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = file.name.replace('.csv', '.json');
link.click();
URL.revokeObjectURL(url); // Free memory
},
error: (error) => {
console.error('Parse error:', error.message);
}
});
});The header: true option is what makes the output usable JSON. Without it, PapaParse returns an array of arrays β each row is just a list of positional values with no keys. With it, each row becomes a named object.
Streaming Large Files Without Crashing the Browser
Loading a 100MB CSV as a single string will exceed Chrome's V8 heap limit on low-memory devices. The heap cap is 1.4GB on 32-bit builds and up to 4GB on 64-bit, but practical tab memory limits kick in much earlier.
PapaParse's chunk callback processes the file in configurable batches and releases each chunk from memory after the callback returns:
const allRows: Record<string, unknown>[] = [];
Papa.parse(file, {
header: true,
skipEmptyLines: true,
dynamicTyping: true,
chunkSize: 1024 * 1024, // 1MB chunks (roughly 10,000β50,000 rows)
chunk: (results, parser) => {
// Accumulate rows β or process and discard if memory is tight
allRows.push(...results.data);
// Example: pause parsing if downstream processing is slow
// parser.pause();
// ... do async work ...
// parser.resume();
},
complete: () => {
const json = JSON.stringify(allRows, null, 2);
downloadJson(json, 'output.json');
console.log('Total rows:', allRows.length);
},
error: (error) => {
console.error('Stream error:', error.message);
}
});Controlling the JSON Output Shape
By default the output is a flat array of objects β one object per CSV row. That works for simple pipelines. But sometimes you need a different shape.
Grouping Rows by a Column Value
// Input CSV:
// department,name,salary
// Engineering,Alice,95000
// Marketing,Bob,72000
// Engineering,Carol,88000
// Group by department after parsing
function groupByColumn(
rows: Record<string, unknown>[],
key: string
): Record<string, unknown[]> {
return rows.reduce((acc, row) => {
const groupKey = String(row[key] ?? 'unknown');
if (!acc[groupKey]) acc[groupKey] = [];
acc[groupKey].push(row);
return acc;
}, {} as Record<string, unknown[]>);
}
// Result:
// {
// "Engineering": [{ name: "Alice", salary: 95000 }, ...],
// "Marketing": [{ name: "Bob", salary: 72000 }]
// }Keyed by ID (Map-style JSON)
// Transform array into a lookup map keyed by a unique column
function keyById(
rows: Record<string, unknown>[],
idColumn: string
): Record<string, unknown> {
return Object.fromEntries(
rows.map(row => [String(row[idColumn]), row])
);
}
// Input: [{ id: "u1", name: "Alice" }, { id: "u2", name: "Bob" }]
// Output: { "u1": { id: "u1", name: "Alice" }, "u2": { ... } }Edge Cases That Break Most CSV Parsers
Rows with Inconsistent Column Counts
A common real-world problem: some rows in the CSV have more or fewer columns than the header. This happens when data was exported from different sources and concatenated together. PapaParse's errors array in each result object catches these:
Papa.parse(file, {
header: true,
complete: (results) => {
// Check for malformed rows
if (results.errors.length > 0) {
console.warn('Parse warnings:');
results.errors.forEach(err => {
console.warn(`Row ${err.row}: ${err.message} (type: ${err.type})`);
});
}
// Filter out any rows that triggered errors (optional)
const cleanRows = results.data.filter((_, i) =>
!results.errors.some(e => e.row === i)
);
console.log('Clean rows:', cleanRows.length);
}
});Mixed Types in the Same Column
When dynamicTyping: true is enabled, PapaParse converts"42" to 42 and"true" to true. This causes problems if a column contains mostly numbers but has some empty cells or string outliers β the resulting JSON array will have mixed types (number | null | string).
Turn off dynamicTyping and do explicit type coercion after parsing:
Papa.parse(file, {
header: true,
dynamicTyping: false, // Keep everything as strings first
complete: (results) => {
const rows = results.data as Record<string, string>[];
const typed = rows.map(row => ({
id: parseInt(row.id, 10) || null,
price: parseFloat(row.price) || 0,
name: row.name?.trim() || '',
active: row.active === 'true' || row.active === '1',
}));
console.log(typed);
}
});Missing BOM Breaks Accented Characters
If your CSV was exported from Excel without the UTF-8 BOM, accented characters will parse as garbled bytes. PapaParse handles BOM stripping automatically when given aFileobject, but if you're passing raw text strings, strip the BOM manually:
// If you already have the CSV as a string (e.g., fetched via XHR)
const BOM = 'ο»Ώ';
const cleanCsv = rawCsvString.startsWith(BOM)
? rawCsvString.slice(1)
: rawCsvString;
Papa.parse(cleanCsv, {
header: true,
// ... rest of config
});JSON Number Precision Loss for Large IDs
JavaScript's Number type is a 64-bit float. It can only represent integers precisely up to 253 β 1 (about 9 quadrillion). If your CSV contains 64-bit database IDs β common in PostgreSQL BIGINT columns βdynamicTyping: true will silently corrupt them.
// Dangerous: PapaParse converts this silently // CSV: id // 9007199254740993 dynamicTyping: true β row.id === 9007199254740992 // WRONG (precision lost!) // Safe: keep as string, use BigInt if you need arithmetic dynamicTyping: false β row.id === "9007199254740993" // correct
When to Use Each Approach
| File Size | Recommended Approach | Why |
|---|---|---|
| Under 5MB | Full parse, single call | Fast, simple, no memory concerns |
| 5MB β 100MB | Chunk streaming, 1MB chunks | Avoids memory spikes, keeps UI responsive |
| Over 100MB | Stream + process-and-discard per chunk | Accumulating all rows in memory will crash low-RAM devices |
Always turn off dynamicTyping if your CSV contains 64-bit IDs or columns with inconsistent types. Do your own coercion with explicit type checks after parsing. It adds ten lines of code and saves you an afternoon of debugging corrupted data.
If you are hitting Excel encoding problems before you even get to parsing, read our guide to CSV encoding issues β specifically the BOM fix and CRLF line-ending section. And if you need to understand how CSV fits into the broader data format landscape, our XML vs JSON comparison covers the API-level format decisions that sit above the file-conversion layer.
Convert CSV to JSON locally β no size limits.
Drop any CSV file and get clean JSON output. Runs in your browser. No upload. No account.
Open CSV to JSON Converter β