Converters

How to Convert JSON to XML (Without Losing Your Data)

How to convert JSON to XML: the mapping rules, a worked example, why keys get renamed, how arrays and nulls are handled, and the escaping mistakes that corrupt values.

6 min readUpdated Sep 7, 2026

Converting JSON to XML sounds like a formatting change but is really a translation between two formats that do not describe the same thing. JSON has arrays, nulls and keys that can be any string. XML has attributes, exactly one root element, a narrow rule about element names, and no concept of a list or an empty value. Every converter must choose where those do not line up, and the difference between a good one and a bad one is whether it tells you what it chose. This guide covers the mapping, a worked example, and the places a value quietly changes.

The mapping in five rules

Almost every JSON-to-XML converter, including the JSON to XML tool here, works from the same five rules:

  • An object becomes an element, and each of its keys becomes a child element.
  • A string, number or boolean becomes the text inside an element.
  • An array becomes repeated sibling elements with the same name - the closest thing XML has to a list.
  • A key marked with @ becomes an attribute rather than a child element, so "@id": "A-4471" gives id="A-4471" on the opening tag.
  • A key called #text becomes the element's own text, which is how an element carries attributes and a value at the same time.

The last two rules are a convention rather than a standard, but they are the most widely used one, and they matter because they are reversible: read backwards, they are how an XML to JSON converter decides what was an attribute.

A worked example

A small order payload:

{ "order": { "@id": "A-4471", "customer": { "name": "Priya Nair", "city": "Pune" }, "line": [ { "@sku": "TSH-01", "qty": "2", "price": "499.00" }, { "@sku": "MUG-07", "qty": "1", "price": "249.50" } ] } }

Applying the rules gives:

<order id="A-4471"> <customer> <name>Priya Nair</name> <city>Pune</city> </customer> <line sku="TSH-01"> <qty>2</qty> <price>499.00</price> </line> <line sku="MUG-07"> <qty>1</qty> <price>249.50</price> </line> </order>

Three things worth noticing: the single top-level key became the root element, the two-entry line array became two <line> elements rather than a wrapper, and the price 249.50 kept its trailing zero - not automatic.

Choosing the root element

An XML document must have exactly one root element and JSON has no such rule. When your JSON is an object with a single top-level key, that key names the root and nothing is invented. When it has several keys, or is an array, a wrapper has to be added:

{"first name":"Priya","order id":4471,"tags":[],"note":null} becomes <root> <first_name>Priya</first_name> <order_id>4471</order_id> <note/> </root>

That one small input carries four losses: two keys renamed, one array vanished, one null now indistinguishable from an empty string. A converter worth using names all four; the next three sections take them in turn.

Keys that XML will not accept

An XML element name cannot start with a digit or contain a space or most punctuation. JSON keys have no such restriction, so "first name" has to become <first_name> and "2024" has to become <_2024>. There is no way round this - it is the format, not the tool - so the only question is whether you are told.

The case to watch for is subtler. If two keys in the same object sanitise to the same name, they become two elements sharing a name, and any reader hands that back as a two-item array rather than two fields:

{"first name":"a","first_name":"b"} becomes <first_name>a</first_name><first_name>b</first_name>

Two distinct fields have silently merged into a list, which is worth an explicit warning rather than a footnote. The same name under two different parents is not a collision, so flagging those is just noise.

Arrays: the two cases that bite

Repeated siblings work for an ordinary list, but two array shapes have no clean XML form. An empty array produces no elements at all, so "tags": [] simply disappears and becomes indistinguishable from a missing key. And an array sitting directly inside another has no key to take a name from, so its entries need one invented:

{"grid":[["a","b"],["c"]]} becomes <root> <grid> <item>a</item> <item>b</item> </grid> <grid> <item>c</item> </grid> </root>

Here <item> came from the converter, not your data. It is the right answer - flattening both levels into one run of siblings would destroy the structure - but you should know a name was invented and be able to change it.

What to do with null

XML has no null, so there are three honest options and the right one depends on who reads the file. An empty tag, <note/>, is simplest and reads back as an empty string. The attribute xsi:nil="true" marks the element as genuinely absent, which is what a schema-aware reader expects, at the cost of one namespace declaration on the root. Or the element can be left out, so the key is simply missing. Guessing is the one thing a converter should not do here.

The escaping that most converters get wrong

Ampersands and angle brackets obviously need escaping. Two less obvious rules decide whether your values survive. First, a literal > has to be escaped too: the sequence ]]> is a parse error in XML content, so a value containing it produces a document that will not load. Second, an XML parser normalises whitespace before your code sees it - a carriage return becomes a line feed everywhere, and inside an attribute value a tab or newline becomes a plain space - so those must be written &#13;, &#9; and &#10; or they are silently lost, and only on the far side of the parser, which makes it hard to trace.

Some characters cannot be carried at all: most control codes are invalid in XML 1.0, escaped or not, so a converter that writes &#1; produces a file no parser will read.

Numbers, IDs and trailing zeros

This is the failure that costs real money. A converter written the quick way parses your JSON with JSON.parse, routing every number through a double-precision float. Anything past about 9 quadrillion is rounded, so a 20-digit order ID loses its last digits, and 1.50 becomes 1.5 because a float has no memory of a trailing zero. Version pins, invoice totals and database IDs are exactly what people paste into a converter, and all three are damaged.

The fix is to keep each number as the text it was written as and emit that unchanged, which is what the JSON to XML converter does - 1.50 stays 1.50 and every digit of a long ID survives. Evaluating another tool? Paste a 20-digit number in and check the output first.

Going back the other way

Because the attribute and text conventions are symmetrical, output from one direction feeds straight into the other: run a document through XML to JSON and back through JSON to XML and you land on the same tree. If the XML will not parse, the XML Formatter gives the exact line and column, and the JSON Formatter validates the JSON before you convert it.

It runs entirely in your browser

API payloads and config files carry customer names, order values, hostnames and tokens, so where a conversion happens is not a detail. This converter runs inside your browser tab and nothing is uploaded, so you can paste a live response without it leaving your device.

Frequently asked questions

How do I make a JSON key become an XML attribute?
Prefix it with @. The key "@id": "A-4471" becomes id="A-4471" on the element's opening tag instead of a child element, and the key #text becomes the element's own text so it can carry attributes and a value together. You can switch the marker to _ or $, or turn attributes off entirely so that every key becomes an element. The convention matters because it is reversible - the same marker read backwards is how an XML to JSON converter decides what was an attribute, so a document can go both ways without losing anything.
Why does my JSON need a wrapper element?
Because XML allows exactly one root element and JSON does not promise one. If your JSON is an object with a single top-level key, that key names the root and nothing is invented. If it has several top-level keys, or if it is an array, there is no single name to use and a wrapper - <root> by default, though you can rename it - has to be added around everything. A converter should tell you when it added one, since the wrapper is not part of your data.
Will a long ID or a price like 1.50 survive the conversion?
It depends entirely on the tool. Any converter that parses your JSON with JSON.parse first pushes every number through a double-precision float, which rounds integers past roughly 9 quadrillion and drops trailing zeros, so a 20-digit order ID loses its last digits and 1.50 comes out as 1.5. A converter that keeps each number as the text you wrote and emits that text unchanged does not have this problem. It is worth testing with a 20-digit number before trusting any converter with invoice totals or database IDs.