Toolmingo
Guides6 min read

How to Convert CSV to JSON (With Code Examples)

Learn how to parse CSV files and convert them to JSON arrays in JavaScript, Python, Go, and PHP — handling headers, quoted fields, type coercion, and edge cases.

CSV files are everywhere — spreadsheet exports, database dumps, data feeds. JSON is the universal format for APIs, web apps, and modern data pipelines. Converting CSV to JSON is a task every developer runs into sooner or later, and it's trickier than it looks.

Why CSV parsing is harder than splitting on commas

The naive approach — line.split(",") — breaks immediately on real-world data:

name,bio,score
Alice,"Developer, designer",42
Bob,"Says ""hello""",17

Two problems here:

  1. Quoted fields can contain commas — "Developer, designer" is one value, not two.
  2. Escaped quotes use double-quote syntax — "" inside a quoted field means a literal ".

The correct parser must handle both. Use a battle-tested library whenever one is available.

The conversion algorithm

A correct CSV-to-JSON converter does four things:

  1. Split lines — handle both \n (Unix) and \r\n (Windows) line endings.
  2. Parse each line — respect quoted fields and escaped quotes (RFC 4180).
  3. Map columns to keys — use the first row as header names.
  4. Return an array of objects — one object per data row.

Type coercion: strings vs numbers

CSV has no types — everything is a string. You have two choices:

Option Result Best for
String-only { "score": "42" } Safe, no data loss
Auto-coerce { "score": 42 } APIs, databases, analytics

Auto-coercion converts numeric strings to numbers and "true"/"false" to booleans. Use it carefully — a column named phone with value "0123456789" should stay a string.

Quick reference

Input Parsed as Notes
Alice "Alice" Plain string
"Alice, Smith" "Alice, Smith" Quoted field with comma
"Says ""hi""" "Says \"hi\"" Escaped quote
42 42 (if coercing) Numeric string
Alice "Alice" Trim whitespace
(empty) null or "" Empty field

JavaScript

Browser / Node.js (manual parser)

function csvToJson(csv, { coerce = false } = {}) {
  const lines = csv.replace(/\r\n/g, "\n").split("\n").filter(Boolean);
  if (lines.length === 0) return [];

  const headers = parseRow(lines[0]);

  return lines.slice(1).map((line) => {
    const values = parseRow(line);
    return Object.fromEntries(
      headers.map((key, i) => [key.trim(), coerceValue(values[i] ?? "", coerce)])
    );
  });
}

function parseRow(line) {
  const fields = [];
  let field = "";
  let inQuotes = false;

  for (let i = 0; i < line.length; i++) {
    const ch = line[i];
    if (inQuotes) {
      if (ch === '"' && line[i + 1] === '"') {
        field += '"';
        i++; // skip second quote
      } else if (ch === '"') {
        inQuotes = false;
      } else {
        field += ch;
      }
    } else {
      if (ch === '"') {
        inQuotes = true;
      } else if (ch === ",") {
        fields.push(field);
        field = "";
      } else {
        field += ch;
      }
    }
  }
  fields.push(field);
  return fields;
}

function coerceValue(val, coerce) {
  if (!coerce) return val;
  if (val === "") return null;
  if (val === "true") return true;
  if (val === "false") return false;
  const num = Number(val);
  return isNaN(num) ? val : num;
}

// Usage
const csv = `name,score,active
Alice,42,true
Bob,17,false`;

console.log(csvToJson(csv, { coerce: true }));
// [{ name: "Alice", score: 42, active: true }, { name: "Bob", score: 17, active: false }]

Node.js with a library (recommended for production)

import { parse } from "csv-parse/sync";

const csv = `name,score
Alice,42
Bob,17`;

const records = parse(csv, {
  columns: true,       // use first row as keys
  skip_empty_lines: true,
  cast: true,          // auto-coerce numbers/booleans
  trim: true,
});

console.log(JSON.stringify(records, null, 2));

Install: npm install csv-parse


Python

Standard library (csv module)

import csv
import json
import io

def csv_to_json(csv_text: str, coerce: bool = False) -> list[dict]:
    reader = csv.DictReader(io.StringIO(csv_text))
    rows = []
    for row in reader:
        if coerce:
            row = {k: coerce_value(v) for k, v in row.items()}
        rows.append(dict(row))
    return rows

def coerce_value(val: str):
    if val == "":
        return None
    if val.lower() == "true":
        return True
    if val.lower() == "false":
        return False
    try:
        return int(val)
    except ValueError:
        pass
    try:
        return float(val)
    except ValueError:
        pass
    return val

csv_text = """name,score,active
Alice,42,true
Bob,17,false"""

result = csv_to_json(csv_text, coerce=True)
print(json.dumps(result, indent=2))

pandas (for data science workflows)

import pandas as pd
import json

df = pd.read_csv("data.csv")
result = json.loads(df.to_json(orient="records"))
print(json.dumps(result, indent=2))

pandas handles type inference automatically — numeric columns become int64/float64, not strings.


Go

package main

import (
	"encoding/csv"
	"encoding/json"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func csvToJSON(csvText string) ([]map[string]any, error) {
	r := csv.NewReader(strings.NewReader(csvText))
	r.TrimLeadingSpace = true

	records, err := r.ReadAll()
	if err != nil {
		return nil, err
	}
	if len(records) == 0 {
		return nil, nil
	}

	headers := records[0]
	rows := make([]map[string]any, 0, len(records)-1)

	for _, record := range records[1:] {
		obj := make(map[string]any, len(headers))
		for i, key := range headers {
			val := ""
			if i < len(record) {
				val = record[i]
			}
			obj[key] = coerce(val)
		}
		rows = append(rows, obj)
	}
	return rows, nil
}

func coerce(s string) any {
	if s == "" {
		return nil
	}
	if s == "true" {
		return true
	}
	if s == "false" {
		return false
	}
	if n, err := strconv.Atoi(s); err == nil {
		return n
	}
	if f, err := strconv.ParseFloat(s, 64); err == nil {
		return f
	}
	return s
}

func main() {
	input := "name,score,active\nAlice,42,true\nBob,17,false"
	rows, err := csvToJSON(input)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	out, _ := json.MarshalIndent(rows, "", "  ")
	fmt.Println(string(out))
}

Go's encoding/csv handles quoting and escaping automatically — you only need to build the header→value mapping yourself.


PHP

<?php
function csvToJson(string $csv, bool $coerce = false): string {
    $lines = array_filter(explode("\n", str_replace("\r\n", "\n", $csv)));
    $lines = array_values($lines);
    if (count($lines) === 0) return '[]';

    $headers = str_getcsv($lines[0]);
    $rows = [];

    foreach (array_slice($lines, 1) as $line) {
        $values = str_getcsv($line);
        $row = [];
        foreach ($headers as $i => $key) {
            $val = $values[$i] ?? '';
            $row[trim($key)] = $coerce ? coerceValue($val) : $val;
        }
        $rows[] = $row;
    }

    return json_encode($rows, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}

function coerceValue(string $val): mixed {
    if ($val === '') return null;
    if (strtolower($val) === 'true') return true;
    if (strtolower($val) === 'false') return false;
    if (is_numeric($val)) return $val + 0; // int or float
    return $val;
}

$csv = "name,score,active\nAlice,42,true\nBob,17,false";
echo csvToJson($csv, coerce: true);

PHP's str_getcsv() handles RFC 4180 quoting correctly — don't use explode(",", $line).


Handling edge cases

Empty fields

An empty CSV cell (,,) should produce null (if coercing) or "" (string-only mode). Never silently drop the key.

Missing columns

If a row has fewer columns than the header, fill the missing values with null rather than throwing an error.

BOM (Byte Order Mark)

Excel-generated CSV files sometimes start with a UTF-8 BOM (\xEF\xBB\xBF). Strip it before parsing:

csv = csv.replace(/^\uFEFF/, "");

Windows line endings

Always normalise \r\n\n before splitting lines, or use a proper CSV library that handles it.

Large files

For files over 50 MB, use streaming parsers instead of loading the whole file into memory:

  • Node.js: csv-parse with streams
  • Python: iterate over csv.DictReader without .read() first
  • Go: call r.Read() in a loop instead of r.ReadAll()

Frequently asked questions

Does CSV have a standard?
Yes — RFC 4180. It defines quoting, escaping, and line endings. Real-world CSV often deviates (semicolons as delimiters in European locales, tabs in TSV), so a good parser should let you configure the delimiter.

What delimiter should I use?
Comma (,) is the default. Semicolons (;) are common in European countries where commas are used as decimal separators. Tabs (\t) create TSV (Tab-Separated Values). Always detect or let the user specify.

How do I handle CSV with a semicolon delimiter?
Pass delimiter: ";" to your CSV library, or replace ; with , in a pre-processing step (only safe if no quoted fields contain semicolons).

Why does my JSON have all string values?
CSV has no types — everything is a string by default. Enable type coercion in your parser to convert "42"42 and "true"true.

How do I convert a CSV file (not a string)?
Read the file first: fs.readFileSync("data.csv", "utf8") in Node, open("data.csv") in Python, os.Open("data.csv") in Go. Then pass the string/reader to your parser.

Can I convert CSV to JSON in the browser?
Yes — the File API lets you read local files: use FileReader.readAsText(file) to get the CSV string, then parse it with the JavaScript example above.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools