How to Convert XML to JSON (And Back) Without Losing Data
XML and JSON hold the same kind of information in very different shapes. XML wraps everything in opening and closing tags and can hang extra data off a tag as attributes. JSON uses keys, braces and square brackets, and has no concept of an attribute at all. Converting between them is mostly mechanical — until you hit the places where the two formats genuinely disagree.
For a straightforward file the job takes about five seconds. Paste your XML into the free XML ↔ JSON Converter, press Convert, copy the JSON. It runs entirely inside your browser tab, so an XML export full of customer records or API keys never leaves your machine — nothing is uploaded to a server.
The part worth reading is what happens to the awkward cases: attributes, lists that contain exactly one item, values like 07030 that look numeric but are not, and text mixed in among child tags. Those are the four places a conversion quietly changes your data rather than failing loudly, and they are the reason "it converted fine" and "it converted correctly" are not the same sentence.
XML vs JSON: What Actually Changes
Before converting anything, it helps to know which parts of XML have a clean JSON equivalent and which do not.
| XML feature | JSON equivalent | Clean? |
|---|---|---|
<name>Ana</name> | "name": "Ana" | Yes |
| Nested tags | Nested objects | Yes |
| Repeated sibling tags | Array | Only if repeated 2+ times |
Attributes id="42" | No equivalent — needs a naming convention | No |
| Text mixed with child tags | No equivalent | No |
| Comments, DOCTYPE, processing instructions | None — JSON has no comments | No |
Namespaces ns:item | Key becomes the literal string "ns:item" | Workable |
The pattern is clear enough: XML can express things JSON cannot. Converting XML to JSON is therefore a lossy operation by default, and converting the JSON back to XML will not reproduce the original file byte for byte. If you need a perfect round trip, keep the original XML.
The reverse direction is easier. Almost everything in JSON has an obvious XML shape — objects become nested elements, arrays become repeated tags. The only real problem is that JSON allows things XML forbids in a tag name, which is covered further down.
How to Convert XML to JSON (Step by Step)
- Open the converter. Go to the XML ↔ JSON Converter. Nothing to install, no account needed.
- Make sure the mode is "XML to JSON". There are two buttons at the top. The left one, XML to JSON, is selected by default. Switching modes clears the output box, so pick the direction before you paste.
- Paste your XML into the left box. This is a paste-in tool, not an upload tool — there is no file picker. If your XML is in a
.xmlfile, open it in a text editor (or drag it into a browser tab), select all, and paste. That is also why nothing touches a server: the text never leaves the page. - Press Convert. The button sits under both boxes. Conversion is not live as you type — you press it once, and again after any edit.
- Copy the JSON. Use the Copy link above the right-hand box. It confirms with a "Copied" tick for two seconds. There is no download button, so paste it straight into your editor and save from there.
Errors appear as a red bar under the boxes reading "Invalid XML input." Note the important limitation described in the trap section below: this message fires far less often than you would expect, because the parser is deliberately forgiving.
If the JSON that comes out is a wall of unindented text after you have pasted it somewhere else, run it through the JSON Formatter to re-indent and validate it, or the JSON Viewer to browse it as a collapsible tree.
What the Conversion Does to Your Data
Here is exactly how the converter maps XML onto JSON. Every example below is real input and real output from the tool.
Nested tags become nested objects
<?xml version="1.0" encoding="UTF-8"?>
<note>
<to>Ana</to>
<body><p>hi</p></body>
</note>
{
"?xml": "",
"note": {
"to": "Ana",
"body": { "p": "hi" }
}
}
Straightforward, with one piece of debris: the XML declaration line becomes an empty "?xml" key. It is harmless but meaningless — delete it before using the JSON anywhere real.
Repeated tags become an array
<catalog><book>A</book><book>B</book><book>C</book></catalog>
{ "catalog": { "book": [ "A", "B", "C" ] } }
Numbers and booleans are converted, not quoted
<r><qty>25</qty><price>19.99</price><ok>true</ok></r>
{ "r": { "qty": 25, "price": 19.99, "ok": true } }
XML has no types — everything in an XML document is text. The converter guesses, and for genuine numbers and true/false the guess is what you want. For values that only look numeric, it is not; see Trap 3.
Empty and self-closing tags become empty strings
<r><a></a><b/><c> spaced </c></r>
{ "r": { "a": "", "b": "", "c": "spaced" } }
Note the third one: surrounding whitespace is trimmed off every value. If your XML holds text where leading spaces are significant — code snippets, fixed-width records, poetry — they are gone.
Entities are decoded, CDATA is unwrapped
<r><t>Tom & Jerry</t><u>5 < 10</u></r>
{ "r": { "t": "Tom & Jerry", "u": "5 < 10" } }
This is correct behaviour. & is XML's way of writing a literal ampersand, and JSON needs no such escape, so it becomes a plain &. Content inside <![CDATA[ ... ]]> is likewise unwrapped to its raw text.
Namespace prefixes stay in the key name
<ns:root xmlns:ns="http://x"><ns:item>1</ns:item></ns:root>
{ "ns:root": { "ns:item": 1 } }
The colon is legal in a JSON key, so this works — but remember that data["ns:item"] is the only way to read it in most languages. Dot access will not.
The Five Traps That Silently Corrupt Data
None of these throw an error. Each one produces JSON that looks fine and is wrong. This is the section to read twice.
Trap 1 — Attributes are dropped
This is the big one. XML attributes have no JSON equivalent, and rather than invent one, the converter discards them:
<book id="42" lang="en"><title>Dune</title></book>
{ "book": { "title": "Dune" } }
The id and lang are gone, with no warning. If your XML carries meaningful data in attributes — and a great deal of real-world XML does, especially API responses, RSS feeds, SVG and configuration files — check the output for it before you rely on the result.
The workaround: promote the attributes into child elements before pasting. Rewrite <book id="42"> as <book><id>42</id>, and the value survives as "id": 42. For a handful of records, do it by hand in your editor. For a large export, a find-and-replace with a regular expression will do it in one pass — the Regex Tester is a good place to get the pattern right before you run it on the real file.
Trap 2 — A list of one is not an array
This one breaks production code more often than any other conversion bug. Compare:
<catalog><book>A</book><book>B</book></catalog>
→ { "catalog": { "book": [ "A", "B" ] } } ← array
<catalog><book>A</book></catalog>
→ { "catalog": { "book": "A" } } ← string, not an array
XML cannot tell the difference between "a list that happens to contain one book" and "a single book" — both are written identically. So the converter guesses from what it sees, and the shape of your JSON changes depending on how many records the file happened to contain.
The consequence: code written against a two-record test file, doing data.catalog.book.forEach(...), crashes the day a customer uploads a one-record file. Always normalise on the way in — in JavaScript, const books = [].concat(data.catalog.book ?? []) gives you an array in every case.
Trap 3 — Leading zeros are destroyed
Because the converter turns numeric-looking text into real numbers, anything that is formatted as digits but is not a quantity gets mangled:
<r><zip>07030</zip><phone>+15551234</phone></r>
{ "r": { "zip": 7030, "phone": 15551234 } }
A New Jersey ZIP code became a four-digit number. The + on the phone number vanished. The same applies to product SKUs, account numbers, invoice references, ISBNs, German postcodes and anything else where a leading zero carries meaning. It is the identical failure mode that mangles ZIP codes when you open a CSV in Excel, and it is just as silent.
Check any ID-like field in the output before trusting it. If a value should be text, re-quote it manually, or handle it downstream as a string.
Trap 4 — Mixed content loses its spacing
When a tag contains both text and child tags, JSON has nowhere sensible to put it. The text fragments get collected into a #text key — and joined without their separating spaces:
<p>Hello <b>world</b> now</p>
{ "p": { "b": "world", "#text": "Hellonow" } }
"Hello" and "now" have been concatenated into Hellonow, and the original word order is unrecoverable from the JSON. If your XML is document-flavoured — XHTML, DocBook, formatted descriptions with inline <em> or <a> tags — do not convert it to JSON at all. Store the fragment as an escaped XML string instead and parse it where you render it.
Trap 5 — Broken XML often does not error
The parser is forgiving by design, so malformed XML frequently converts without complaint:
<a><b>unclosed</a> → { "a": { "b": "unclosed" } }
hello world → { }
(empty input) → { }
The mismatched closing tag in the first line is a genuine XML error, and it still produces output. Plain text produces an empty object rather than an error. So an empty {} is the real failure signal here — if you press Convert and get {} or {"?xml": ""} back, your input was not valid XML, whatever the absence of a red error bar suggests. Empty output is a result you should never accept at face value.
How to Convert JSON to XML
The reverse direction uses the same page — click JSON to XML at the top, paste JSON on the left, press Convert. Output is indented automatically.
{ "catalog": { "book": [ { "title": "A" }, { "title": "B" } ] } }
<catalog>
<book>
<title>A</title>
</book>
<book>
<title>B</title>
</book>
</catalog>
Arrays become repeated sibling tags, which is exactly right. Characters that XML requires you to escape are escaped correctly — Tom & Jerry comes out as Tom & Jerry, 5 < 10 as 5 < 10 — so you can paste text values in without pre-processing them.
Four things to know before you use the output:
- Your JSON needs exactly one top-level key. XML requires a single root element. Feed in
{"a":1,"b":2}and you get<a>1</a><b>2</b>— two roots, which is not valid XML. Wrap your data in one outer key first:{"root":{"a":1,"b":2}}. - A top-level array produces numeric tag names.
[{"a":1},{"a":2}]becomes<0>...</0><1>...</1>. Tag names cannot begin with a digit, so this output is invalid XML and most parsers will reject it. Wrap the array in a named key instead:{"items":[{"a":1},{"a":2}]}. - Keys must be legal XML names.
{"my key":"v"}produces<my key>v</my key>, which will not parse — spaces are not allowed in tag names. Rename such keys tomy_keybefore converting. The same applies to keys starting with a digit. - Empty arrays disappear and there is no XML declaration. A key whose value is
[]is dropped entirely from the output rather than producing an empty tag, andnullbecomes a self-closing<key/>. The output also has no<?xml version="1.0"?>line — add it yourself if the consuming system expects one.
If your JSON does not parse, the tool shows "Invalid JSON input." Unlike the XML direction, this check is strict and reliable — a trailing comma or single-quoted key will be caught. Run questionable JSON through the JSON Formatter first to see exactly where the syntax breaks.
Converting XML to JSON in Code
For a one-off file, the browser tool is faster than writing anything. For a recurring job, here is the equivalent in the four environments people ask about most. All of them keep attributes if you configure them to, which is the main advantage over a quick paste-and-convert.
JavaScript / Node.js
npm install fast-xml-parser
import { XMLParser } from 'fast-xml-parser';
const parser = new XMLParser({
ignoreAttributes: false, // keep attributes
attributeNamePrefix: '@_', // as "@_id", "@_lang"
parseTagValue: false, // keep "07030" a string
});
const json = parser.parse(xmlString);
Those three options turn off the three traps above. ignoreAttributes: false preserves attributes, and parseTagValue: false leaves every value as text so leading zeros survive.
Python
pip install xmltodict
import xmltodict, json
with open('data.xml') as f:
data = xmltodict.parse(f.read()) # attributes kept as "@id"
print(json.dumps(data, indent=2))
xmltodict keeps attributes by default with an @ prefix and never coerces types — every value stays a string. For bulk conversions where correctness matters more than convenience, this is the safest of the four.
PHP
$xml = simplexml_load_file('data.xml');
$json = json_encode($xml, JSON_PRETTY_PRINT);
Short, but be aware it moves attributes into an @attributes sub-object and has the same one-item-is-not-an-array behaviour described in Trap 2.
Command line
brew install yq # or: apt install yq
yq -p=xml -o=json data.xml > data.json
Useful in a shell pipeline or a cron job. yq keeps attributes with a +@ prefix by default.
Tips
- Convert a small sample first. Take twenty representative records, convert them, and read the JSON by eye against the XML. Ten minutes here catches every trap in this guide before it reaches a 200MB export.
- Search the output for the values you care about. Faster than reading the whole thing: pick three or four important attribute values and IDs from your XML and Ctrl+F for them in the JSON. Anything missing tells you immediately what got dropped.
- Watch for numbers with leading zeros. ZIPs, SKUs, invoice numbers, account references, ISBNs. If a field is an identifier rather than a quantity, it should be a string.
- Delete the
?xmlkey. It carries nothing and it will confuse anything that iterates over top-level keys. - Normalise arrays on the way in, not everywhere downstream. One helper that coerces single objects into single-item arrays beats defensive checks scattered through your codebase.
- Keep the original XML. Converting back does not reproduce it. Attributes, comments and the declaration are gone for good.
- Format after converting. Paste the result into the JSON Formatter to validate and pretty-print, or the JSON Viewer to explore a large structure as a collapsible tree.
- Going further than JSON? The same data can go on to a spreadsheet with the JSON to CSV converter, or to config-friendly YAML with the YAML ↔ JSON Converter.
FAQs
Is converting XML to JSON lossless? ▼
What happens to XML attributes when I convert to JSON? ▼
@_id. FileNaut's browser converter currently drops them, so <book id="42"><title>Dune</title></book> becomes {"book":{"title":"Dune"}}. If your attributes hold real data, promote them to child elements in your XML first — <book><id>42</id> — or use a library-based conversion with ignoreAttributes: false.Why did my ZIP code 07030 turn into 7030? ▼
<qty>25</qty> and wrong for anything that only looks numeric. Leading zeros disappear, and a leading + on a phone number is stripped. Check every ID-style field after converting, and re-quote it as a string if the formatting matters.Why is my array a plain string when there is only one record? ▼
[].concat(data.catalog.book ?? []) always yields an array. This is the single most common cause of code that works in testing and crashes in production.Can I convert a large XML file, and is my data uploaded anywhere? ▼
I converted JSON to XML and the result will not parse. Why? ▼
<0>, which are illegal. Or a key contained a space or started with a digit, which XML forbids in a tag name. Wrap your data in a single named root key and rename any offending keys before converting.The converter returned an empty {} but showed no error. What went wrong? ▼
{} — or an output containing only "?xml": "" — as the failure signal. Check that you pasted the whole document including its root element, and that the mode toggle is set to XML to JSON rather than JSON to XML.Should I convert my XML API responses to JSON permanently? ▼
Is the XML to JSON converter free? ▼
Ready to try it?
Use the tool right now — free, no signup, no upload.