Toolmingo
Guides7 min read

How to Convert XML to JSON (With Code Examples)

Learn how to parse XML and output JSON in JavaScript, Python, Go, and PHP — handling attributes, text nodes, arrays, and namespaces correctly.

XML still powers a huge portion of the web — RSS/Atom feeds, SOAP APIs, Android manifests, Maven pom.xml, Microsoft Office files, SVG, and countless enterprise integrations. JSON is what modern frontends and microservices speak. Converting between them is a routine task, but XML's data model doesn't map cleanly to JSON's, so naive conversions frequently lose information or produce surprises.

This guide explains the tricky parts and shows working code in JavaScript, Python, Go, and PHP.

Why XML-to-JSON is tricky

XML has features that have no JSON equivalent:

XML feature Problem
Attributes (<tag id="1">) JSON has no concept of "attribute vs element"
Text nodes mixed with children <p>Hello <b>world</b></p> — text and elements are siblings
Repeated elements <item>a</item><item>b</item> — should become an array, not get overwritten
Namespaces <ns:tag xmlns:ns="..."> — the prefix is part of the name
Comments and PIs Usually ignored, but some formats use them
Character data (CDATA) <![CDATA[raw content]]> — should be treated as text

There's no single official XML-to-JSON mapping. The most widely adopted convention (popularised by the Badgerfish and xml2js formats) is:

  • Attributes → stored under a @attributes (or $) key
  • Text content → stored under a #text (or _) key
  • Repeated sibling elements → collected into an array
  • Single-child elements → kept as an object (some libraries optionally force arrays)

The conversion algorithm

  1. Parse the XML into a tree (DOM or event-based)
  2. Walk the tree recursively
  3. For each element, build a JS/Python/Go/PHP object:
    • Add each attribute as @attributes.<name>: value
    • Add each child element as a property (accumulate duplicates into an array)
    • If the element has only text content, store it as #text or collapse the object to a plain string
  4. Serialize the result to JSON

JavaScript

Node.js doesn't include an XML parser in its standard library, but the fast-xml-parser package (npm, ~600 kB, zero dependencies) is the de-facto choice:

import { XMLParser } from "fast-xml-parser";

const xml = `
<books>
  <book id="1">
    <title>Clean Code</title>
    <author>Robert Martin</author>
  </book>
  <book id="2">
    <title>The Pragmatic Programmer</title>
    <author>Hunt &amp; Thomas</author>
  </book>
</books>`;

const parser = new XMLParser({
  ignoreAttributes: false,       // keep id="1" etc.
  attributeNamePrefix: "@_",    // prefix to distinguish attrs from elements
  isArray: (name) =>            // force these tags to always be arrays
    ["book"].includes(name),
});

const result = parser.parse(xml);
console.log(JSON.stringify(result, null, 2));

Output:

{
  "books": {
    "book": [
      { "@_id": "1", "title": "Clean Code", "author": "Robert Martin" },
      { "@_id": "2", "title": "The Pragmatic Programmer", "author": "Hunt & Thomas" }
    ]
  }
}

In a browser, use DOMParser with a manual tree-walk:

function xmlNodeToJson(node) {
  if (node.nodeType === Node.TEXT_NODE) {
    const text = node.nodeValue.trim();
    return text ? text : null;
  }

  const obj = {};

  // attributes
  for (const attr of node.attributes ?? []) {
    obj[`@${attr.name}`] = attr.value;
  }

  // children
  for (const child of node.childNodes) {
    if (child.nodeType === Node.COMMENT_NODE) continue;
    const key = child.nodeName === "#text" ? "#text" : child.nodeName;
    const val = xmlNodeToJson(child);
    if (val === null) continue;
    if (key in obj) {
      obj[key] = [].concat(obj[key], val); // duplicate → array
    } else {
      obj[key] = val;
    }
  }

  // collapse plain-text elements
  const keys = Object.keys(obj);
  if (keys.length === 1 && keys[0] === "#text") return obj["#text"];

  return obj;
}

const doc = new DOMParser().parseFromString(xml, "text/xml");
const json = { [doc.documentElement.nodeName]: xmlNodeToJson(doc.documentElement) };
console.log(JSON.stringify(json, null, 2));

Python

The standard library's xml.etree.ElementTree is sufficient for most XML. For full attribute + namespace support, use xmltodict (pip):

import xmltodict
import json

xml = """
<books>
  <book id="1">
    <title>Clean Code</title>
    <author>Robert Martin</author>
  </book>
  <book id="2">
    <title>The Pragmatic Programmer</title>
    <author>Hunt &amp; Thomas</author>
  </book>
</books>
"""

data = xmltodict.parse(xml, force_list={"book"})
print(json.dumps(data, indent=2))

force_list={"book"} ensures book is always a list — even when there's only one element. Without it, a single <book> would parse as a dict and two would parse as a list, which breaks consuming code.

Using the standard library only:

import xml.etree.ElementTree as ET
import json

def elem_to_dict(elem):
    result = {}
    # attributes
    if elem.attrib:
        result["@attributes"] = dict(elem.attrib)
    # text
    text = (elem.text or "").strip()
    if text:
        result["#text"] = text
    # children
    for child in elem:
        key = child.tag
        val = elem_to_dict(child)
        if key in result:
            existing = result[key]
            result[key] = existing if isinstance(existing, list) else [existing]
            result[key].append(val)
        else:
            result[key] = val
    # collapse if only text
    if list(result.keys()) == ["#text"]:
        return result["#text"]
    return result

root = ET.fromstring(xml.strip())
print(json.dumps({root.tag: elem_to_dict(root)}, indent=2))

Go

Go's encoding/xml package can unmarshal into a map[string]interface{} with some work. The github.com/basgys/goxml2json library handles the common case cleanly:

package main

import (
    "fmt"
    "strings"

    xml2json "github.com/basgys/goxml2json"
)

func main() {
    xmlStr := `
    <books>
      <book id="1"><title>Clean Code</title><author>Robert Martin</author></book>
      <book id="2"><title>The Pragmatic Programmer</title><author>Hunt &amp; Thomas</author></book>
    </books>`

    json, err := xml2json.Convert(strings.NewReader(xmlStr))
    if err != nil {
        panic(err)
    }
    fmt.Println(json.String())
}

For a zero-dependency approach using only the standard library:

package main

import (
    "encoding/json"
    "encoding/xml"
    "fmt"
    "io"
    "strings"
)

type Node struct {
    XMLName  xml.Name
    Attrs    []xml.Attr `xml:",any,attr"`
    Content  []byte     `xml:",innerxml"`
    Children []Node     `xml:",any"`
}

func (n Node) MarshalJSON() ([]byte, error) {
    m := map[string]interface{}{}
    for _, a := range n.Attrs {
        m["@"+a.Name.Local] = a.Value
    }
    for _, c := range n.Children {
        key := c.XMLName.Local
        val := interface{}(c)
        if existing, ok := m[key]; ok {
            switch e := existing.(type) {
            case []interface{}:
                m[key] = append(e, val)
            default:
                m[key] = []interface{}{existing, val}
            }
        } else {
            m[key] = val
        }
    }
    if len(m) == 0 {
        return json.Marshal(strings.TrimSpace(string(n.Content)))
    }
    return json.Marshal(m)
}

func main() {
    xmlStr := `<books><book id="1"><title>Clean Code</title></book></books>`
    dec := xml.NewDecoder(strings.NewReader(xmlStr))
    var root Node
    if err := dec.Decode(&root); err != nil && err != io.EOF {
        panic(err)
    }
    out, _ := json.MarshalIndent(map[string]interface{}{root.XMLName.Local: root}, "", "  ")
    fmt.Println(string(out))
}

PHP

PHP's SimpleXML is the easiest path. It doesn't handle duplicate elements gracefully, but works for most feeds and configs:

<?php

function xmlToArray(SimpleXMLElement $xml): mixed {
    $result = [];

    // attributes
    foreach ($xml->attributes() as $key => $val) {
        $result['@' . $key] = (string) $val;
    }

    // children
    foreach ($xml->children() as $tag => $child) {
        $val = xmlToArray($child);
        if (isset($result[$tag])) {
            // already exists — make it an array
            $result[$tag] = array_merge(
                is_array($result[$tag]) && !array_key_exists('@', $result[$tag])
                    ? $result[$tag]
                    : [$result[$tag]],
                [$val]
            );
        } else {
            $result[$tag] = $val;
        }
    }

    // text-only node
    if (empty($result)) {
        return trim((string) $xml) ?: null;
    }

    $text = trim((string) $xml);
    if ($text !== '') {
        $result['#text'] = $text;
    }

    return $result;
}

$xmlString = <<<XML
<books>
  <book id="1"><title>Clean Code</title><author>Robert Martin</author></book>
  <book id="2"><title>The Pragmatic Programmer</title><author>Hunt &amp; Thomas</author></book>
</books>
XML;

$xml = simplexml_load_string($xmlString);
$array = [
    $xml->getName() => xmlToArray($xml),
];

echo json_encode($array, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

For better namespace handling, use DOMDocument with DOMXPath.

Edge-case quick reference

Input XML pattern What to do
<item> appears once Wrap in array if consuming code expects an array — use force_list / isArray option
Attribute id="1" Store as @id or @_id to distinguish from child elements
<p>Hello <b>world</b></p> Mixed content — store text node as #text, keep children
<ns:tag xmlns:ns="uri"> Strip prefix or include it — depends on whether namespace matters to consumers
<![CDATA[raw]]> Most parsers expand CDATA transparently — treat as text
Empty element <br /> Map to null, "", or {} — pick one and be consistent
Numeric text <count>5</count> Most parsers keep as string; coerce manually if needed

Common pitfalls

1. Losing attributes. By default, many quick conversions use text() content only and silently drop attributes. Always check if your XML uses attributes before choosing a library or snippet.

2. Duplicate elements collapsing. When two <item> elements appear, a naive object builder overwrites the first with the second. Always accumulate duplicates into an array.

3. Single-element arrays. Some feeds have one <item> today and ten tomorrow. If you don't force-list those elements, your consuming code breaks when the count changes. Use force_list / isArray options, or normalise after parsing.

4. Namespace prefixes. <atom:entry> and <entry> may refer to the same element depending on xmlns declarations. Stripping prefixes naively can cause key collisions.

5. Encoding issues. XML declares its encoding in the prolog (<?xml version="1.0" encoding="ISO-8859-1"?>). Most parsers handle this, but reading the file as bytes and then parsing as UTF-8 will corrupt non-ASCII characters.

6. Pretty-printed vs minified. Whitespace-only text nodes (\n ) can appear as #text: "\n " between elements. Always trim text content and discard blank text nodes.

Frequently asked questions

Does the conversion lose any information?
Not if you preserve attributes (@name) and text nodes (#text). The only truly lossy conversion is ignoring attributes or treating mixed content as pure text.

What's the best library for Node.js?
fast-xml-parser for most work. xml2js is older but still widely used. For tiny scripts, the browser's DOMParser or Node's @xmldom/xmldom also works.

How do I handle a SOAP envelope?
Strip the <soap:Envelope> and <soap:Body> wrappers, then convert the inner payload. Focus on the namespace-local name (Body, not soap:Body) if you're stripping prefixes.

Can I round-trip JSON back to XML?
Yes, but only if you preserved the @attributes / #text structure. Most tools support JSON-to-XML as well as XML-to-JSON.

Why do some converters produce {"book": {...}} and others produce {"book": [...]}?
Whether a single element becomes an object or a one-item array depends on the library's default behaviour and whether you configure a force_list option. Pick the convention that matches your consumer's expectations and stick with it.

What about very large XML files (100 MB+)?
Use a streaming (SAX/event-based) parser instead of loading the whole document into memory. In Python, xml.etree.ElementTree.iterparse; in Go, xml.Decoder.Token(); in Node, sax or expat-stream.

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