The two models do not line up: RFC 8259 defines six JSON types with no attributes, while XML 1.0 has attributes, namespaces and ordered mixed content. Going this direction means choosing what to do with the difference.
The first time I had to send JSON data into a system that only spoke XML, I did the naive thing: I hand-wrote the angle brackets in a text editor. It was a supplier feed that expected a SOAP-ish envelope, and my source was a tidy JSON array coming out of a Laravel API. Twenty minutes in, I had mismatched a closing tag somewhere around the fortieth record and the receiving parser threw a "junk after document element" error that told me nothing about where. That evening taught me a lesson I have applied on every integration since: converting between JSON and XML is not a creative act. It is a mapping problem with a small number of rules, and the moment you write those rules down, the whole thing becomes mechanical.
This guide is the version of that mapping I wish I'd had. I build toolz.dev, and one of the tools there is a browser-based JSON to XML converter that applies exactly the conventions below. But the point of this article is not the button - it's understanding why a key becomes an element, why an @-prefixed key becomes an attribute, and why an array turns into repeated tags rather than a numbered list. Once those three ideas click, you can convert any JSON to XML by hand if you have to, and you can debug the output when a downstream system rejects it.
TL;DR: To convert JSON to XML, map each object key to an element, each
@-prefixed key to an attribute on its parent element, and the#textkey to the element's text content. Expand every array into repeated sibling elements that share the key's name. Escape&,<, and>in text (plus"in attribute values), and sanitize any key that isn't a legal XML name. Do it in the browser so payloads with tokens or customer data never leave your machine.
Why would you convert JSON to XML in the first place?
JSON won the web API war years ago, and if you spend your days in React, Laravel, or Node, you might reasonably ask when you'd ever need XML at all. The answer is: constantly, just not in the places you look. XML is the lingua franca of a huge installed base of systems that predate the JSON era and are not going anywhere. SOAP web services - still the backbone of banking, insurance, logistics, and government integrations - carry their payloads in XML. RSS and Atom feeds are XML. Sitemaps are XML. Android layouts, .docx and .xlsx internals (Office Open XML), SVG, RSS podcast feeds, and countless B2B EDI-style exchanges are all XML.
So the real-world scenario is almost always the same shape: you have data in JSON because that's what your stack produces, and you need it in XML because that's what the other side demands. Maybe you're pushing product data into a marketplace that only accepts an XML feed. Maybe you're wrapping an API response in a SOAP body. Maybe you're generating an RSS feed from a JSON content export. In every case, you don't want to reinvent the serialization each time - you want a predictable rule that turns any JSON structure into valid XML, so you can automate it and stop thinking about it.
There's also a quieter reason: readability during debugging. When you're staring at a deeply nested JSON blob trying to understand a hierarchy, converting it to indented XML sometimes makes the tree structure jump out, because XML's open/close tags make nesting explicit in a way that JSON's braces don't. I keep the JSON to XML converter and the reverse XML to JSON converter open in adjacent tabs more often than I'd have guessed.
How does JSON map to XML, exactly?
Here is the core of it. There are only four rules, and everything else is a detail.
Rule 1: Object keys become elements. A JSON object { "book": { "title": "..." } } becomes <book><title>...</title></book>. The key is the tag name; the value is what goes inside.
Rule 2: @-prefixed keys become attributes. XML elements can carry attributes, and JSON has no native concept of them, so we need a convention. The widely used one - and the one my tool uses - is a prefix character, @ by default. So { "book": { "@id": "bk101", "title": "..." } } becomes <book id="bk101"><title>...</title></book>. Attributes can only hold primitive values (strings, numbers, booleans), never nested structures, which matches how XML attributes actually work.
Rule 3: The #text key becomes text content. When an element needs both attributes and text - think <title lang="en">Hello</title> - you can't express that with a plain string value, because the string leaves no room for the attribute. The convention is a reserved key, #text: { "title": { "@lang": "en", "#text": "Hello" } }. If an element only has text and no attributes, you can skip #text and just use a plain string value.
Rule 4: Arrays become repeated elements. This is the one people get wrong most often. XML has no array type. A list of things is expressed as repeated sibling elements with the same tag name. So { "tags": { "tag": ["computer", "web"] } } becomes <tags><tag>computer</tag><tag>web</tag></tags> - not <tag>0</tag> or any index-based nonsense. The array's key supplies the repeated tag name.
Put those together on a realistic object and the output is exactly what a downstream XML parser expects:
{
"catalog": {
"book": [
{ "@id": "bk101", "author": "Gambardella, Matthew", "price": 44.95 },
{ "@id": "bk102", "author": "Ralls, Kim", "price": 5.95 }
]
}
}
becomes
<?xml version="1.0" encoding="UTF-8"?>
<catalog>
<book id="bk101">
<author>Gambardella, Matthew</author>
<price>44.95</price>
</book>
<book id="bk102">
<author>Ralls, Kim</author>
<price>5.95</price>
</book>
</catalog>
Notice that the single top-level key, catalog, became the document's root element. That's deliberate: a well-formed XML document must have exactly one root. When your JSON already has a single wrapping key, that key is the root. When it doesn't - when you hand the converter a multi-key object or a bare array - the tool wraps everything under a configurable root element (root by default) so the output stays well-formed.
What about escaping and invalid names?
Two things quietly break more conversions than any nesting bug: unescaped special characters and illegal element names.
Escaping. XML reserves a handful of characters. Inside element text, &, <, and > must be written as &, <, and >. Inside a double-quoted attribute value, you additionally have to escape the double quote as ". If your JSON string contains Tom & Jerry and you drop it into XML raw, the bare ampersand makes the document malformed and the parser dies. A correct converter escapes automatically, so "a < b & c" becomes a < b & c in the output and round-trips back to the original text when parsed. This is not optional polish - it's the difference between valid and invalid XML.
Element names. XML has strict rules about what a tag name may contain. Names can't include spaces, can't start with a digit, a hyphen, or a period, and exclude most punctuation. JSON keys have no such restrictions - "first name", "123", and "total($)" are all perfectly legal JSON keys and all illegal XML names. A converter that ignores this produces documents that no parser will accept. The pragmatic fix, and what my tool does, is to sanitize: replace illegal characters with underscores and prepend an underscore when a name starts with a digit. So "123 bad" becomes <_123_bad>. It's not glamorous, but it guarantees the output parses, which is the entire point.
How do I use the browser-based converter?
The workflow on toolz.dev/tools/json-to-xml mirrors the rules above, with a few options for the practical edge cases.
Paste your JSON into the input and press Convert. If your JSON is malformed, you get a clear parse error rather than silent garbage - I lean on the browser's own JSON.parse, so the error messages match what you'd see in your console. Choose 2 or 4 spaces of indentation when you want a human-readable document for review, or pick Minify to collapse the whole thing onto a single line when you're shipping it over the wire and every byte counts (SOAP requests and feed payloads especially). Set the root element name for the case where your JSON has no single wrapping key. Toggle the XML declaration (<?xml version="1.0" encoding="UTF-8"?>) on or off depending on whether the consumer expects a prolog. And decide whether empty nodes should self-close as <tag/> or expand to <tag></tag> - some strict consumers care.
Everything runs client-side. The converter is a dependency-free serializer written in TypeScript, not a wrapper around a remote API. That matters more than it sounds: API responses and config files routinely contain access tokens, customer records, and internal identifiers, and a "free online converter" that POSTs your payload to someone's server is a data-leak waiting to happen. Because this one never makes a network call, you can convert sensitive data safely, and it keeps working with no connection at all. If privacy in browser tools is something you think about - and if you handle other people's data, it should be - I wrote more about it in the data privacy tools guide.
JSON vs XML: a quick comparison
It helps to keep the two formats' trade-offs in view, because the reason the mapping needs conventions like @ and #text is that XML can express things JSON can't, and vice versa.
| Aspect | JSON | XML |
|---|---|---|
| Attributes | No native concept | First-class (<tag attr="v">) |
| Arrays / lists | Native [ ] type |
Repeated sibling elements |
| Comments | Not allowed | <!-- ... --> supported |
| Namespaces | None | Full namespace support |
| Mixed content (text + elements) | Awkward | Native |
| Schema / validation | JSON Schema (add-on) | XSD, DTD, RELAX NG (mature) |
| Verbosity | Compact | More verbose (closing tags) |
| Typical use today | Web APIs, config | SOAP, feeds, documents, enterprise |
The @ prefix exists to bridge the "attributes" row; the repeated-tag rule bridges the "arrays" row; and #text bridges the "mixed content" row. Once you see the mapping as a bridge across these specific gaps, it stops feeling arbitrary.
Does JSON to XML round-trip cleanly?
Mostly, yes - and that's by design. My JSON to XML and XML to JSON tools share the same @ attribute prefix and #text content key, so converting XML → JSON and back generally reproduces the original document. If you're building a pipeline that has to move data both directions, that symmetry is worth relying on.
Where round-tripping gets fuzzy is the same place every JSON/XML mapping gets fuzzy: order and mixed content. JSON objects are officially unordered, so a converter may not preserve the exact sibling order of differently-named elements. XML that interleaves text and child elements (<p>Hello <b>world</b>!</p>) doesn't have a clean JSON representation and comes back as an approximation. And an element that sometimes appears once and sometimes appears multiple times is ambiguous - is it a single value or an array of one? These aren't bugs in any particular tool; they're inherent to the fact that the two data models don't perfectly overlap. Knowing where the seams are lets you design your JSON so the conversion stays lossless: be consistent about whether a thing is always an array, and avoid mixed content where you can.
If you work with JSON a lot, it's worth building fluency across the whole family of conversions - I collected the ones I reach for most in the ultimate guide to JSON tools, and the broader coding tools guide covers where format converters fit into a day-to-day workflow. For the reverse direction and adjacent formats, the JSON to YAML converter and a plain JSON formatter round out the set.
Common mistakes when converting JSON to XML
A few traps I've hit or watched others hit:
Treating array indices as tag names. If you see <item0>, <item1> in someone's output, they built the converter wrong. Arrays become repeated tags with the same name, taken from the array's key.
Forgetting there can be only one root. Handing a multi-key object straight to a serializer without wrapping it produces multiple top-level elements, which is not a well-formed document. Wrap it.
Skipping escaping "because the data looks clean." It looks clean until one product description contains an ampersand or a <. Always escape; never assume.
Putting structured data in attributes. Attributes hold primitives. If you try to shove a nested object into an @-prefixed key, a correct converter will (and should) drop it or ignore it, because there's no valid XML for it. Model it as a child element instead.
Advanced: choosing indentation and declaration for the consumer
One habit that has saved me repeated back-and-forth with integration partners: match the output exactly to what the consumer expects, then stop. Some SOAP endpoints reject a document that includes a byte-order mark or an unexpected prolog; others require the <?xml ... ?> declaration and 415 you without it. Some feed validators want pretty-printed, indented XML for their own debugging; most production transports want it minified. Rather than argue, I generate whichever variant the spec asks for. That's why the converter exposes indentation (including a minify option), the declaration toggle, and self-closing behavior as first-class controls - they're not decoration, they're the knobs that determine whether a picky consumer accepts your document on the first try.
FAQ
How do I convert JSON to XML?
Paste your JSON into the editor and press Convert. Object keys become XML elements, arrays become repeated tags, and the result appears ready to copy or download. There is no upload - the conversion happens in your browser.
How are JSON attributes represented in XML?
By convention, any object key that starts with the "@" prefix is written as an attribute on its parent element rather than as a child element. For example, {"book": {"@id": "bk101", "title": "..."}} becomes
How does the converter handle JSON arrays?
Each element of an array is emitted as a repeated sibling element sharing the key's name. So {"tags": {"tag": ["a", "b"]}} produces
What is the #text key for?
When an element needs both attributes and text content, the text is stored under the "#text" key. {"title": {"@lang": "en", "#text": "Hello"}} becomes
Can I get minified XML instead of indented output?
Yes. Set the indentation to 0 (minify) and the converter emits the whole document on a single line with no whitespace between tags. This is useful for SOAP requests or feeds where payload size matters; switch back to 2 or 4 spaces when you need a readable document.
What happens to keys that are not valid XML element names?
XML element names cannot contain spaces, cannot begin with a digit, hyphen or period, and exclude most punctuation. Keys that violate these rules are sanitized - invalid characters become underscores and a leading underscore is added when needed - so the output always parses, even if your JSON keys were not XML-friendly.
Does JSON to XML round-trip with the XML to JSON tool?
For the common structures it does. This tool and the XML to JSON tool share the same "@" attribute prefix and "#text" content key, so converting XML to JSON and back generally reproduces the same document. Order-independent details and mixed content are the usual sources of small differences, as they are with any XML/JSON mapping.
Is it safe to convert sensitive JSON here?
Yes. The converter is plain JavaScript that runs entirely in your browser - nothing you paste is transmitted to a server, logged, or stored. That makes it safe for API responses, config files and records containing tokens or personal data, and it keeps working with no network connection.



