← All articles

How to Convert CSV and XML to JSON Without Losing Shape

Convert CSV and XML to JSON with practical mapping rules for headers, types, attributes, repeated elements, namespaces, and browser-safe validation.

CSV and XML both end up in JSON pipelines for the same reason: the source system is older, more spreadsheet-shaped, or more enterprise-shaped than the code consuming it. A vendor sends a CSV export. A SOAP service returns XML. An RSS feed needs to become an API response. The mechanical part is easy. The data-shape decisions are where bugs appear.

The short version:

  • CSV -> JSON usually becomes an array of objects, one object per row.
  • XML -> JSON usually becomes one root object, with attributes, text nodes, and repeated elements mapped by convention.
  • CSV values start as text. Type coercion is optional and should be conservative.
  • XML has concepts JSON does not: attributes, namespaces, CDATA, comments, mixed content, and repeated element names.
  • After converting, validate the JSON and inspect the shape before handing it to another service.

This is the broad conversion guide. For deeper single-topic guides, see How to Convert JSON to CSV, XML to JSON, and JSON to XML.

Quick Mapping Table

Source shape Natural JSON shape Watch for
CSV header row object keys duplicate or empty headers
CSV data row one object in an array missing cells, extra cells
CSV value string, or carefully coerced scalar ZIP codes, IDs, large integers
XML root element one top-level key multiple roots are not normal XML documents
XML attribute @-prefixed key, such as @id prefix convention must be documented
XML text with attributes #text mixed content can be lossy
Repeated XML elements array one item vs many items changes shape
XML namespace prefix keep prefix in key, such as soap:Envelope stripping prefixes can create collisions

If you remember only one thing: CSV is a table; XML is a tree. JSON can represent both, but the mapping choices are different.

CSV To JSON: The Row Mapping

A typical CSV has a header row followed by records:

id,name,active,zip
1,Ada,true,02139
2,Bob,false,94105

The usual JSON output is:

[
  { "id": 1, "name": "Ada", "active": true, "zip": "02139" },
  { "id": 2, "name": "Bob", "active": false, "zip": "94105" }
]

Notice the deliberate type choice. id can safely become a number here. active can become a boolean. zip should stay a string because leading zeroes matter.

That is why "convert everything that looks numeric" is risky. A good CSV converter either leaves every value as a string or uses conservative coercion.

Do Not Parse Real CSV With split(',')

This works only for toy CSV:

const [header, ...rows] = csv.trim().split('\n');
const keys = header.split(',');

It fails the moment a field contains a comma, a quote, or a newline:

id,name,note
1,Ada,"Loves compilers, math, and notes"
2,Bob,"Line one
line two"
3,Carol,"He said ""ship it"""

The field Loves compilers, math, and notes is one cell, not four. The newline inside Bob's note is part of the quoted value, not the end of the row. The doubled quote in Carol's note represents a literal quote.

Use a parser. In browser or Node projects, PapaParse is the usual production choice:

import Papa from 'papaparse';

const result = Papa.parse(csvText, {
  header: true,
  skipEmptyLines: true,
});

console.log(result.data);

If you are building a small local tool, a simple state machine can still be enough. The key is that it must track whether it is inside quotes:

function parseCsvGrid(text) {
  const rows = [];
  let row = [];
  let field = '';
  let inQuotes = false;

  for (let i = 0; i < text.length; i++) {
    const char = text[i];

    if (inQuotes) {
      if (char === '"' && text[i + 1] === '"') {
        field += '"';
        i++;
      } else if (char === '"') {
        inQuotes = false;
      } else {
        field += char;
      }
      continue;
    }

    if (char === '"') inQuotes = true;
    else if (char === ',') { row.push(field); field = ''; }
    else if (char === '\n') { row.push(field); rows.push(row); row = []; field = ''; }
    else if (char !== '\r') field += char;
  }

  if (field !== '' || row.length) {
    row.push(field);
    rows.push(row);
  }

  return rows;
}

That is close to the dependency-free approach used by fixjson.org's CSV converter: quote-aware parsing, BOM stripping, header row to object keys, and light type coercion only when it is safe.

CSV Type Coercion: Be Conservative

CSV itself has no types. Every cell is text. JSON has strings, numbers, booleans, null, arrays, and objects. The conversion step has to decide whether to keep text as text or coerce it.

A safe coercion policy looks like this:

CSV cell JSON value Why
true true exact boolean token
false false exact boolean token
null null exact null token
42 42 number round-trips safely
3.14 3.14 number round-trips safely
007 "007" leading zero may be meaningful
9007199254740993 "9007199254740993" too large for safe JavaScript integer precision
empty cell "" empty string is safer than guessing null

In JavaScript:

function coerceCsvCell(value) {
  if (value === '') return '';
  if (value === 'true') return true;
  if (value === 'false') return false;
  if (value === 'null') return null;

  if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(value)) {
    const number = Number(value);
    if (Number.isFinite(number) && String(number) === value) {
      return number;
    }
  }

  return value;
}

That String(number) === value check is small but important. It keeps 007 as a string and avoids pretending unsafe large integers survived as exact numbers.

CSV To JSON In Python

Python's standard library handles CSV quoting correctly:

import csv
import json

with open("customers.csv", newline="", encoding="utf-8-sig") as file:
    rows = list(csv.DictReader(file))

print(json.dumps(rows, indent=2, ensure_ascii=False))

Two details are doing useful work:

  • newline="" lets the csv module handle line endings correctly.
  • encoding="utf-8-sig" strips a UTF-8 BOM if Excel or another Windows tool added one.

csv.DictReader returns strings. If you want numbers and booleans, add a deliberate coercion pass field by field.

CSV Edge Cases That Break Imports

Duplicate Headers

This CSV is ambiguous:

id,name,name
1,Ada,Lovelace

An object cannot keep both name values under the same key. Decide whether to reject duplicate headers, rename them (name, name_2), or collect duplicates into arrays. Silent overwrite is the worst option.

Missing And Extra Cells

Rows do not always match the header:

id,name,active
1,Ada,true
2,Bob
3,Carol,false,extra

Common policies:

  • Missing cells become "".
  • Extra cells are rejected.
  • Extra cells are stored under a reserved key such as _extra.

Pick one and document it. Imports fail later when every row has a slightly different shape.

Delimiters That Are Not Commas

"CSV" often means "spreadsheet text export." It may be comma-separated, tab-separated, or semicolon-separated:

  • US-style CSV: id,name,active
  • TSV: id\tname\tactive
  • European Excel export: id;name;active

If the output turns into one giant column, the delimiter is wrong. Let the user choose the delimiter or use a parser with delimiter detection.

XML To JSON: The Tree Mapping

XML is not a table. It is an element tree with attributes and text:

<user id="1" active="true">
  <name>Ada</name>
  <role>admin</role>
  <role>editor</role>
  <note priority="high">Review access</note>
</user>

A practical JSON mapping is:

{
  "user": {
    "@id": "1",
    "@active": "true",
    "name": "Ada",
    "role": ["admin", "editor"],
    "note": {
      "@priority": "high",
      "#text": "Review access"
    }
  }
}

This is the same convention used by many XML-to-object tools:

  • attributes -> @ keys
  • element text with attributes or children -> #text
  • repeated child elements -> arrays
  • root element -> top-level JSON key

There is no universal XML-to-JSON standard. The convention matters because downstream code will depend on it.

XML Attributes vs Child Elements

XML gives you two ways to say "id":

<user id="1">
  <name>Ada</name>
</user>

and:

<user>
  <id>1</id>
  <name>Ada</name>
</user>

Those are not identical XML shapes. In JSON, keep them distinct:

{
  "user": {
    "@id": "1",
    "name": "Ada"
  }
}

versus:

{
  "user": {
    "id": "1",
    "name": "Ada"
  }
}

The @ prefix prevents an attribute named id from colliding with a child element named <id>.

The Single-Item Array Problem

This is the XML-to-JSON bug that shows up in production:

<roles>
  <role>admin</role>
</roles>

often becomes:

{ "roles": { "role": "admin" } }

but:

<roles>
  <role>admin</role>
  <role>editor</role>
</roles>

becomes:

{ "roles": { "role": ["admin", "editor"] } }

The field changes type depending on the data. If consumers expect role to always be an array, normalize after parsing:

const roles = [].concat(doc.roles?.role ?? []);

For a schema-backed feed, configure your XML parser to always array-ify known repeatable paths. For a generic browser converter, the safest default is to reflect the document shape and explain the convention.

XML Text, CDATA, And Mixed Content

For data-style XML, text is usually simple:

<title>Effective TypeScript</title>

becomes:

{ "title": "Effective TypeScript" }

If the element also has attributes, the text needs a key:

<price currency="USD">9.99</price>

becomes:

{ "price": { "@currency": "USD", "#text": "9.99" } }

CDATA is text too:

<body><![CDATA[Use <strong>care</strong> here]]></body>

should become a string containing Use <strong>care</strong> here.

Mixed content is harder:

<p>Hello <strong>Ada</strong>, welcome back.</p>

Most data converters either concatenate text, drop loose whitespace, or produce a more verbose node list. If your XML is document-style markup, not data-style XML, expect to review the output manually.

XML Namespaces

Namespaces have no direct JSON equivalent:

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>...</soap:Body>
</soap:Envelope>

The safest generic mapping keeps the prefix:

{
  "soap:Envelope": {
    "@xmlns:soap": "http://schemas.xmlsoap.org/soap/envelope/",
    "soap:Body": "..."
  }
}

This looks a little awkward, but it is lossless. Stripping soap: may make keys prettier, but it can merge two different elements that share a local name.

XML Parser Safety

If you parse XML from untrusted sources, pay attention to DTDs and external entities. Server-side XML parsers have a long history of XXE-style bugs when they resolve external entities or process unexpected DTDs.

For browser-side conversion, DOMParser does not fetch arbitrary external entities like an old server parser might, but you still need to treat the output as untrusted data. For backend code:

  • disable DTDs and external entity resolution unless you explicitly need them
  • set size limits before parsing huge documents
  • reject document-style XML if your converter only supports data XML
  • use a maintained parser with safe defaults

The fixjson.org XML converter is intentionally dependency-free and best-effort for typical data XML. It skips processing instructions and DTD-like declarations, maps attributes with @, maps mixed text to #text, and decodes the common XML entities.

Validate The JSON Output

After conversion, run a quick review before the result enters another system:

  1. Does the JSON parse?
  2. Are IDs and ZIP codes still strings if they need leading zeroes?
  3. Did repeated XML elements become arrays where your consumer expects arrays?
  4. Did attributes land under the prefix your code expects?
  5. Did empty cells become "", null, missing keys, or something else?
  6. Are namespaces preserved if you need to round-trip back to XML?

For one-off browser work, paste the converted output into JSON Validator or inspect it in JSON Viewer. If the source is sensitive, use local browser tools rather than uploading customer exports to a random formatter.

Convert In The Browser

fixjson.org has two relevant local tools:

  • JSON to CSV Converter - converts JSON to CSV and CSV back to JSON; CSV parsing respects quoted commas, quoted newlines, doubled quotes, BOM stripping, and conservative type coercion.
  • JSON to XML Converter - converts JSON to XML and XML back to JSON using the @ / #text / repeated-element conventions described above.

Both run in your browser. Your spreadsheet exports, API payloads, and XML feeds are not uploaded to a server.

Frequently Asked Questions

How do I convert CSV to JSON?

Parse the CSV with a quote-aware parser, treat the first row as headers, then map each following row to an object keyed by those headers. Coerce types only when it is safe; IDs, ZIP codes, and large integers often need to stay strings.

Why are all my CSV-to-JSON values strings?

CSV has no native types. Every cell starts as text. A converter may choose to coerce exact true, false, null, and lossless numbers, but ambiguous values such as 007 should remain strings.

How do I handle XML attributes when converting to JSON?

Use a convention that keeps attributes separate from child elements. The common approach is to prefix attributes with @, so <user id="1"> becomes { "user": { "@id": "1" } }.

Why does my XML-to-JSON output sometimes give an object and sometimes an array?

Because many converters use the data itself to decide shape: one <role> becomes a string or object, while two <role> elements become an array. Normalize known repeatable fields after parsing, or configure your parser to always array-ify those paths.

Should I convert XML values to numbers and booleans?

Only if your schema says those fields are numbers or booleans. XML text and attributes arrive as strings. Blind coercion can corrupt IDs, codes, and high-precision values.

Is it safe to convert XML from untrusted users?

Use a parser with DTDs and external entities disabled, set size limits, and treat the converted JSON as untrusted input. XML conversion changes the format; it does not validate business rules.

Convert, Validate, And Format

Sources

Last reviewed July 2026.