Turn an XML feed or export into JSON you can work with
A lot of data still arrives as XML: product feeds, order exports from older shop systems, SOAP responses, RSS and settings files. The code you want to read it with, in JavaScript, Python or an API call, is far happier with JSON. Converting XML to JSON gets you there without writing a parser, and this page's converter is built for exactly that: upload the XML file, download a JSON file with the same data in it.
The two formats don't line up one to one. XML has attributes and JSON doesn't, XML can repeat a tag and JSON uses a list for that, and XML treats every value as text. Each of those needs a rule, and the rules decide what your JSON looks like. They're all below, with a real file we converted ourselves.
How to convert XML to JSON
- Upload your XML file - Drop it into the box at the top of this page, or browse for it. You can add up to 5 files at once.
- Check the output - JSON is already picked on this page, so there's nothing to set.
- Download your file - Save the .json file when it's ready. Any text editor or browser opens it.
The conversion runs on our servers, not in your browser, so there's nothing to install and no account to make. Our security page explains what happens to your file afterwards.
How XML tags become JSON keys
The converter reads the whole document and writes one JSON key for each tag name. The output is indented with two spaces and saved as UTF-8 without a byte order mark, so Hindi, Japanese or Arabic text is written as the characters themselves, not as \u codes. The shape follows these rules:
- The root element becomes the top key. An XML file has one outer tag that holds everything, and the JSON is one object with that tag's name as its only key:
<library>...</library>gives{"library": {...}}. - Attributes become keys with an
@_prefix.<book id="b1">gives"@_id": "b1". The prefix is there so an attribute callednamecan't collide with a child tag calledname. Attributes come first in the object, in the order they appear in the tag, and their values are trimmed. - Text becomes a string, or
#text. A tag with nothing but text inside, like<title>Dune</title>, becomes"title": "Dune". If the tag also has attributes or child tags, the text has to share the object with them, so it goes under the key#text, always last. - Repeated tags become a list. Two or more
<book>tags inside the same parent give"book": [...], with one object per tag, in document order. - Empty tags become an empty string.
<note/>and<note></note>both give"note": "". A tag with attributes and nothing else gives an object holding only those attributes, such as{"@_code": "7"}. - Namespace prefixes stay in the key.
<x:item>gives the key"x:item", an attributex:attrgives"@_x:attr", and thexmlns:xdeclaration itself is kept as"@_xmlns:x", so nothing about the namespace is lost.
Here's a short file that uses the first three rules, and the JSON our live converter gave for it:
<product sku="A1">
<name>Lamp</name>
<price currency="EUR">19.99</price>
<note/>
</product>
{
"product": {
"@_sku": "A1",
"name": "Lamp",
"price": {
"@_currency": "EUR",
"#text": "19.99"
},
"note": ""
}
}
These are the same conventions the fast-xml-parser library uses, and our converter was written to match its output. If you already have code that reads fast-xml-parser's JSON, it reads ours too.
A real example from our tests
On 13 November 2026 we uploaded this file to our live converter. It's a small book catalog: two <book> tags, each with an id and a lang attribute, a price that carries a currency attribute, and an ISBN that starts with a zero.
<?xml version="1.0" encoding="UTF-8"?>
<catalog>
<book id="bk101" lang="en">
<title>The Hobbit</title>
<author>J.R.R. Tolkien</author>
<price currency="USD">8.99</price>
<isbn>0261102214</isbn>
</book>
<book id="bk102" lang="fr">
<title>Le Petit Prince</title>
<author>Antoine de Saint-Exupéry</author>
<price currency="EUR">7.50</price>
<isbn>0156012197</isbn>
</book>
</catalog>
About 17 seconds after we started the upload, it came back as this JSON:
{
"catalog": {
"book": [
{
"@_id": "bk101",
"@_lang": "en",
"title": "The Hobbit",
"author": "J.R.R. Tolkien",
"price": {
"@_currency": "USD",
"#text": "8.99"
},
"isbn": "0261102214"
},
{
"@_id": "bk102",
"@_lang": "fr",
"title": "Le Petit Prince",
"author": "Antoine de Saint-Exupéry",
"price": {
"@_currency": "EUR",
"#text": "7.50"
},
"isbn": "0156012197"
}
]
}
}
Everything landed where the rules say. catalog is the top key. The two attributes on each book became @_id and @_lang, ahead of the child tags. The price had both an attribute and text, so it became an object with @_currency and #text. The two <book> tags became a list of two objects. Every value is a string, which is why 8.99 is in quotes and the ISBN 0261102214 still has its leading zero; a JSON number would have dropped it. The accent in Saint-Exupéry came through as written. You can download the XML we uploaded and the JSON it produced and compare them yourself.
The backend team ran their own production test the same day. A file that declared encoding="ISO-8859-1" instead of UTF-8 converted with São Paulo intact, an orders export with namespaces and CDATA came out as described above, and a file with Japanese, Arabic, Hindi and emoji text came through unchanged.
Things that catch people out
- A tag that appears once is not a list. The converter can't know that
<item>is meant to repeat if a record only has one. So in the file below, the first order has"item": ["Lamp", "Desk"]and the second has"item": "Chair", a plain string. The backend team tested this case, and we ran the file below through our live converter on 13 November 2026 to check it. Code that reads the JSON has to handle both shapes, for example by wrapping a non-list value in a list before looping over it.
<orders>
<order id="1">
<item>Lamp</item>
<item>Desk</item>
</order>
<order id="2">
<item>Chair</item>
</order>
</orders>
{
"orders": {
"order": [
{
"@_id": "1",
"item": [
"Lamp",
"Desk"
]
},
{
"@_id": "2",
"item": "Chair"
}
]
}
}
- Every value is a string. XML has no number type, so the converter doesn't guess.
<qty>2003</qty>gives"2003", not2003, andtruestays the word"true". Convert the fields you know are numbers in your own code, where you also know which ones must keep their zeros. - Text around an inline tag is joined with nothing between. In document-style XML, like
<p>before<b>bold</b>after</p>, the result is"b": "bold"and"#text": "beforeafter". The order of words around the inline tag is lost, so this converter suits data files better than marked-up prose. - Whitespace is trimmed at both ends, and kept inside. The indentation around a value goes away, but line breaks in the middle of a multi-line value stay as written.
- CDATA is kept as plain text. A
<![CDATA[ ... ]]>block comes through exactly as written, spaces included, and any&or<inside it is a normal character in the JSON string. - Comments, processing instructions and the declaration are dropped.
<!-- -->comments, a line like<?xml-stylesheet ?>, and the<?xml version="1.0"?>header have no place in JSON, so they aren't in the output. Built-in and numeric entities are decoded first:&,éand☺give&,éand☺. - A DOCTYPE that declares entities is refused. Entity declarations are how the XXE and "billion laughs" attacks work, so the converter stops with
This XML declares entities, which we don't accept for security reasons.A plain<!DOCTYPE root>line is fine. We fed it both attack files on our test server before launch, and it refused both. An entity the file uses but never declares, such as , givesInvalid XML: undefined entity at line N., which we checked on the test server too. - Other encodings work when the file says which one it uses. UTF-8 is the default, with or without a byte order mark. A file whose header declares another encoding is read in that encoding; ISO-8859-1 was tested on production, Shift_JIS and UTF-16 on our test server before launch. A file that isn't UTF-8 and doesn't declare anything is refused with
Invalid XML: the file isn't UTF-8 text and doesn't say which encoding it uses.
Limits, and files that won't convert
- Each XML file can be up to 20 MB. The upload box takes bigger files, but the converter stops anything over 20 MB with
This file is larger than 20 MB. Try splitting it into smaller files. - A file with more than 2,000,000 elements, or tags nested more than 64 levels deep, is refused. The converted file can't be larger than 200 MB, and a conversion that runs past 960 seconds is stopped. Each of these has its own message asking you to split the file.
- An empty file gives
This file is empty. - Broken XML is refused with the reason and the line, for example
Invalid XML: mismatched tag at line 3.Plain text saved with an .xml name getsInvalid XML: syntax error at line 1.Fix that line in a text editor and upload the file again.
If you want rows and columns rather than nested objects, our XML to CSV converter picks the repeating tag as the rows and flattens each record into dotted column names. And to go the other way, the JSON to XML converter reads the same @_ and #text keys back into attributes and text.