Toolmingo
Guides9 min read

How to Convert CSV to XML (With Code Examples)

Learn how to parse CSV and serialize it to XML in JavaScript, Python, Go, and PHP — with code examples, a quick-reference table, common pitfalls, and FAQ.

CSV is the lowest-common-denominator format for data exports — spreadsheets, databases, and reporting tools all write it. XML is the required format for many enterprise systems, SOAP APIs, Android resource files, and legacy integrations. When you need to move data from one to the other, there's no universal standard for how CSV rows map to XML elements, so you need to understand the shape you're building.

This guide covers the structural choices, working code in JavaScript, Python, Go, and PHP, and the edge cases that break naive converters.

How CSV rows map to XML

CSV has no schema. Each row is flat — a list of column values separated by delimiters. XML is hierarchical. To convert between them you choose a convention:

CSV concept XML representation
File Root element wrapping all records (e.g. <records>)
Row Repeated child element (e.g. <record>)
Column header Child element name or attribute name
Cell value Text content or attribute value
Empty cell Empty element (<city/>) or omit the element

The most common convention uses column headers as child element names:

CSV input:

name,age,city
Alice,30,Podgorica
Bob,25,Budva

XML output:

<?xml version="1.0" encoding="UTF-8"?>
<records>
  <record>
    <name>Alice</name>
    <age>30</age>
    <city>Podgorica</city>
  </record>
  <record>
    <name>Bob</name>
    <age>25</age>
    <city>Budva</city>
  </record>
</records>

The alternative is to put values as attributes (<record name="Alice" age="30" city="Podgorica"/>), which produces more compact XML but loses the ability to represent multi-line or structured values.

Why CSV-to-XML is non-trivial

Problem What goes wrong
Special XML characters &, <, >, ", ' in CSV cells break XML syntax
Invalid element names Column headers like "First Name" or "123id" produce invalid XML tags
Quoted CSV fields "New York, NY" must be parsed as one field, not two
Multi-line fields CSV allows newlines inside quoted fields; naive split-on-newline fails
Encoding CSVs from Excel are often Windows-1252; XML expects UTF-8
Empty headers Extra commas create nameless columns that become invalid XML

JavaScript

Browser (PapaParse + manual XML builder)

import Papa from "papaparse";

function escapeXml(str) {
  return String(str)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&apos;");
}

function sanitizeTag(name) {
  // XML element names must start with letter or _, no spaces
  return name.trim().replace(/\s+/g, "_").replace(/[^a-zA-Z0-9_\-\.]/g, "").replace(/^([^a-zA-Z_])/, "_$1") || "field";
}

function csvToXml(csvText, rootTag = "records", rowTag = "record") {
  const { data, errors } = Papa.parse(csvText, {
    header: true,
    skipEmptyLines: true,
  });
  if (errors.length) throw new Error(errors[0].message);

  const headers = Object.keys(data[0] || {}).map(sanitizeTag);
  const originalHeaders = Object.keys(data[0] || {});

  const rows = data.map((row) => {
    const fields = originalHeaders
      .map((h, i) => `    <${headers[i]}>${escapeXml(row[h] ?? "")}</${headers[i]}>`)
      .join("\n");
    return `  <${rowTag}>\n${fields}\n  </${rowTag}>`;
  });

  return `<?xml version="1.0" encoding="UTF-8"?>\n<${rootTag}>\n${rows.join("\n")}\n</${rootTag}>`;
}

// Usage
const csv = `name,age,city\nAlice,30,Podgorica\nBob,25,Budva`;
console.log(csvToXml(csv));

Node.js (built-in csv-parse + xml2js)

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

function csvToXml(csvText, rootName = "records", rowName = "record") {
  const records = parse(csvText, { columns: true, skip_empty_lines: true });

  // xml2js handles escaping automatically
  const obj = {
    [rootName]: {
      [rowName]: records,
    },
  };

  const builder = new Builder({ xmldec: { version: "1.0", encoding: "UTF-8" } });
  return builder.buildObject(obj);
}

xml2js escapes XML special characters automatically, so you don't need to do it manually. The caveat: column headers become element names as-is, so invalid characters will still produce broken XML.

Python

Standard library (csv + xml.etree.ElementTree)

import csv
import io
import re
import xml.etree.ElementTree as ET

def sanitize_tag(name: str) -> str:
    """Turn an arbitrary string into a valid XML element name."""
    tag = re.sub(r"\s+", "_", name.strip())
    tag = re.sub(r"[^a-zA-Z0-9_\-\.]", "", tag)
    if not tag or not re.match(r"[a-zA-Z_]", tag):
        tag = f"_{tag}"
    return tag or "field"

def csv_to_xml(
    csv_text: str,
    root_tag: str = "records",
    row_tag: str = "record",
    encoding: str = "utf-8",
) -> str:
    reader = csv.DictReader(io.StringIO(csv_text))
    headers = reader.fieldnames or []
    safe_tags = {h: sanitize_tag(h) for h in headers}

    root = ET.Element(root_tag)

    for row in reader:
        record = ET.SubElement(root, row_tag)
        for header in headers:
            child = ET.SubElement(record, safe_tags[header])
            child.text = row.get(header) or ""

    ET.indent(root, space="  ")  # Python 3.9+
    return ET.tostring(root, encoding="unicode", xml_declaration=True)

# Usage
csv_text = "name,age,city\nAlice,30,Podgorica\nBob,25,Budva"
print(csv_to_xml(csv_text))

xml.etree.ElementTree escapes &, <, and > inside text nodes automatically. ET.indent() requires Python 3.9+; on older versions omit it (output will be on one line) or use minidom for pretty-printing.

With pandas (good for large files)

import csv
import io
import xml.etree.ElementTree as ET
import pandas as pd

def csv_to_xml_pandas(path: str, root_tag: str = "records", row_tag: str = "record") -> str:
    df = pd.read_csv(path, dtype=str)  # read everything as string to avoid type loss
    df = df.fillna("")                 # replace NaN with empty string

    root = ET.Element(root_tag)
    for _, row in df.iterrows():
        record = ET.SubElement(root, row_tag)
        for col in df.columns:
            child = ET.SubElement(record, col.replace(" ", "_"))
            child.text = row[col]

    ET.indent(root, space="  ")
    return ET.tostring(root, encoding="unicode", xml_declaration=True)

dtype=str prevents pandas from silently converting "0123" to 123 or "true" to True.

Go

package main

import (
    "bytes"
    "encoding/csv"
    "encoding/xml"
    "fmt"
    "regexp"
    "strings"
)

var (
    reSpaces  = regexp.MustCompile(`\s+`)
    reInvalid = regexp.MustCompile(`[^a-zA-Z0-9_\-\.]`)
    reStart   = regexp.MustCompile(`^[^a-zA-Z_]`)
)

func sanitizeTag(name string) string {
    tag := reSpaces.ReplaceAllString(strings.TrimSpace(name), "_")
    tag = reInvalid.ReplaceAllString(tag, "")
    if tag == "" || reStart.MatchString(tag) {
        tag = "_" + tag
    }
    return tag
}

func csvToXML(csvText, rootTag, rowTag string) (string, error) {
    r := csv.NewReader(strings.NewReader(csvText))
    headers, err := r.Read()
    if err != nil {
        return "", err
    }

    safeTags := make([]string, len(headers))
    for i, h := range headers {
        safeTags[i] = sanitizeTag(h)
    }

    var buf bytes.Buffer
    buf.WriteString(`<?xml version="1.0" encoding="UTF-8"?>` + "\n")
    buf.WriteString("<" + rootTag + ">\n")

    for {
        row, err := r.Read()
        if err != nil {
            break
        }
        buf.WriteString("  <" + rowTag + ">\n")
        for i, val := range row {
            // encoding/xml escapes special characters
            escaped, _ := xml.Marshal(struct {
                XMLName xml.Name `xml:"v"`
                Value   string   `xml:",chardata"`
            }{Value: val})
            inner := strings.TrimPrefix(strings.TrimSuffix(string(escaped), "</v>"), "<v>")
            fmt.Fprintf(&buf, "    <%s>%s</%s>\n", safeTags[i], inner, safeTags[i])
        }
        buf.WriteString("  </" + rowTag + ">\n")
    }

    buf.WriteString("</" + rootTag + ">")
    return buf.String(), nil
}

func main() {
    csvText := "name,age,city\nAlice,30,Podgorica\nBob,25,Budva"
    out, err := csvToXML(csvText, "records", "record")
    if err != nil {
        panic(err)
    }
    fmt.Println(out)
}

Using encoding/xml to marshal cell values ensures that &, <, and > are always properly escaped — don't build the XML by hand with fmt.Sprintf if you don't escape first.

PHP

<?php

function sanitizeTag(string $name): string {
    $tag = preg_replace('/\s+/', '_', trim($name));
    $tag = preg_replace('/[^a-zA-Z0-9_\-\.]/', '', $tag);
    if ($tag === '' || !preg_match('/^[a-zA-Z_]/', $tag)) {
        $tag = '_' . $tag;
    }
    return $tag ?: 'field';
}

function csvToXml(
    string $csvText,
    string $rootTag = 'records',
    string $rowTag = 'record'
): string {
    $lines = array_filter(explode("\n", str_replace("\r\n", "\n", $csvText)));
    $rows = array_map('str_getcsv', array_values($lines));

    if (empty($rows)) {
        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<{$rootTag}/>";
    }

    $headers = array_shift($rows);
    $safeTags = array_map('sanitizeTag', $headers);

    $dom = new DOMDocument('1.0', 'UTF-8');
    $dom->formatOutput = true;
    $root = $dom->createElement($rootTag);
    $dom->appendChild($root);

    foreach ($rows as $row) {
        $record = $dom->createElement($rowTag);
        $root->appendChild($record);
        foreach ($safeTags as $i => $tag) {
            $child = $dom->createElement($tag);
            $child->appendChild($dom->createTextNode($row[$i] ?? ''));
            $record->appendChild($child);
        }
    }

    return $dom->saveXML();
}

// Usage
$csv = "name,age,city\nAlice,30,Podgorica\nBob,25,Budva";
echo csvToXml($csv);

DOMDocument::createTextNode() handles XML escaping automatically. Do not use createElement() for cell values that contain special characters — always go through a text node.

Warning: str_getcsv on a single line cannot handle multi-line quoted fields. For production CSV parsing use fgetcsv with fopen('php://memory', 'r+') or a library like league/csv.

Quick reference

Task JavaScript Python Go PHP
Parse CSV Papa.parse / csv-parse csv.DictReader / pandas.read_csv encoding/csv str_getcsv / fgetcsv
Build XML xml2js.Builder / manual xml.etree.ElementTree encoding/xml DOMDocument
XML escaping Auto (xml2js) / manual Auto (ET) Manual via xml.Marshal Auto (createTextNode)
Sanitize headers Manual regex Manual regex Manual regex Manual regex
Pretty-print xml2js with explicitArray ET.indent (3.9+) Manual formatOutput = true
Large files Streaming PapaParse pandas chunks csv.Reader rows fgetcsv line-by-line

Common pitfalls

1. Not escaping XML special characters If a CSV cell contains &, <, >, ", or ' and you interpolate it directly into XML string templates, you'll produce malformed XML. Always use a library's text-node builder or an explicit escape function.

2. Using column headers as-is Headers like "First Name", "123abc", or "type" are invalid XML element names. "First Name" has a space (not allowed), "123abc" starts with a digit (not allowed), and "type" is a reserved XML attribute name. Sanitize all headers before using them as tag names.

3. Splitting on newlines to parse CSV CSV allows quoted fields that contain newlines: "line1\nline2" is one cell. Splitting on \n will break such files. Use a proper CSV parser in every language.

4. Encoding mismatch (Windows Excel) Excel's "Save as CSV" often produces Windows-1252 encoding, especially in European locales. If you see garbled characters like é instead of é, re-open the file with the correct encoding or use chardet / iconv to detect and convert it to UTF-8 before processing.

5. Assuming one root element per row The default "each row → child element" convention is the most common, but some systems expect one root element per row (one XML document per row). Know your target schema before converting.

6. Empty trailing columns A trailing comma (Alice,30,) adds an empty header or an empty last column. Map "" to a placeholder tag like field or skip empty-header columns entirely rather than producing <> (invalid) in your output.

FAQ

Can I convert CSV to XML without programming? Yes — use the Data Converter on this site. Paste your CSV, select the target format, and download the result. Everything runs in your browser; nothing is uploaded to a server.

What root and row element names should I use? Use names that describe your data: <employees><employee>, <products><product>, <orders><order>. Many enterprise systems specify the required element names in their XSD schema — check the schema or API documentation first.

How do I add XML attributes instead of child elements? Change the mapping so each CSV cell becomes an attribute of the row element: <record name="Alice" age="30" city="Podgorica"/>. This is more compact but attributes can't contain structured content and some XML parsers treat them differently. Use child elements unless the target schema requires attributes.

Can I convert CSV to XML with a schema (XSD)? CSV-to-XML conversion produces a document; validation against an XSD schema is a separate step. In Python use lxml (xmlschema or etree.XMLSchema). In Java use javax.xml.validation. Generate the XML first, then validate it against the schema.

How do I handle CSV files with no header row? Either add a header row before parsing, or generate element names like field1, field2, field3 based on the column index.

My CSV has nested data (JSON in a column). How do I handle it? Parse the JSON value first, then insert it as nested XML elements rather than as a text node. This requires a two-pass approach: CSV parse → row object → for JSON-valued cells, recursively convert to XML child elements.

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