JSON vs XML: Differences and How to Convert Between Them
Learn how JSON and XML data models differ, why conversion is lossy, which mapping rules to use, and how to convert safely with tested code and browser tools.

Sooner or later every developer ends up with data in one format and a system that wants the other. A partner sends a SOAP response and your front end expects JSON. A legacy export is XML and the new service speaks only JSON. A build tool reads XML configuration, and you would rather generate it from a JSON object. It looks like a ten-minute job: swap the angle brackets for braces and move on. Then the first attribute, repeated element or empty tag shows up, and you discover that there is no single correct way to do it.
This guide is for developers, integrators and anyone who has pasted XML into a converter and wondered why the output looked odd. You will learn how the two data models differ, why conversion between them is lossy and sometimes ambiguous, which mapping conventions are widely used, and how to write and test your own converters in JavaScript and Python. You will also see how to convert in the browser with DevUtilX, which security problems come with parsing XML, and how to decide which format a new project should use.
Two formats, two data models
The most important sentence in this whole article is this: JSON and XML are not two spellings of the same thing. They describe data with different building blocks, and every conversion has to translate between those blocks.
JSON has six kinds of value: object, array, string, number, boolean and null. An object is an unordered set of name and value pairs, an array is an ordered list, and everything nests freely. The specification, RFC 8259, defines the grammar in a few pages. JSON values carry their type in the syntax: 42 is a number, "42" is a string, and the parser knows the difference without any schema.
XML is a markup language, and its core model is a tree of nodes. The W3C XML specification defines elements, attributes, character data, comments, processing instructions, CDATA sections and a document type declaration. Elements can contain other elements and text at the same time, which is called mixed content. Every node, in practice, holds text. XML on its own has no numbers or booleans: <qty>3</qty> contains the character 3, and any meaning beyond that comes from a schema or from the application reading it.
Putting the two side by side shows where the friction lies.
| Concept | JSON | XML |
|---|---|---|
| Basic structure | Object, array | Element tree |
| Named values | Object members | Child elements or attributes |
| Ordered lists | Arrays | Repeated sibling elements |
| Text | Strings | Text nodes, CDATA |
| Numbers and booleans | Native types | Text only, typed by a schema |
| Null | null |
No standard form (empty element, or an xsi:nil attribute) |
| Comments | Not allowed | <!-- ... --> |
| Metadata on a value | No equivalent | Attributes |
| Mixed text and elements | No equivalent | Allowed |
| Namespaces | No equivalent | Built in, with prefixes and URIs |
| Document root | Any value | Exactly one root element |
| Child order | Object members are unordered, array items ordered | Document order is significant |
Several rows of that table are the source of nearly every conversion headache. Attributes, mixed content and namespaces have no JSON counterpart. Arrays have no direct XML counterpart. Types exist on one side only. The sections below take them one at a time.
A short history
XML 1.0 became a W3C Recommendation in 1998. It was designed as a simplified subset of SGML, the older and much heavier standard generalised markup language, so that documents could be served and processed on the web with ordinary tools. Its goals were broad: it had to be usable over the internet, support many human languages through Unicode, and be readable by people as well as programs. The result was a format that could describe prose documents, such as books and web pages, as comfortably as data records.
In the years that followed, a large ecosystem grew around it. Namespaces let vocabularies from different organisations share a document. XML Schema described document structure and data types. XSLT transformed one XML document into another. SOAP wrapped remote procedure calls in XML envelopes, and syndication formats such as RSS and Atom used it to publish feeds. Office documents, vector graphics and many build and configuration systems adopted XML too.
JSON arrived from a different direction. It grew out of the object literal syntax of JavaScript, and in the early 2000s it was popularised, notably by Douglas Crockford, as a lightweight way for a browser to receive data from a server. A grammar small enough to learn in an afternoon, direct support in browsers and a shape that matched the objects and arrays of most programming languages made it easy to adopt. As web APIs multiplied, JSON became the default format for them. Our JSON formatting guide covers its syntax in more detail.
The two formats now coexist. XML dominates in document-centred domains and in older enterprise and government integrations. JSON dominates in web and mobile APIs and in modern tooling. That coexistence is why conversion is a daily task, and why it pays to understand what is lost when you do it.
Why conversion is lossy and ambiguous
Converting between formats is a mapping problem. For a faithful conversion you would need a mapping that is reversible, one that returns exactly the original when you convert there and back. Between XML and JSON, no such mapping exists without extra rules, because each side contains things the other cannot express.
Going from XML to JSON, the following information has no natural home:
- Attributes. JSON members belong to an object, and an element's attributes sit beside its children, not inside them. You must invent a convention to keep them apart.
- Mixed content. A paragraph such as
<p>Hello <b>bold</b> world</p>interleaves text and elements. An object cannot represent that sequence. - Comments and processing instructions. JSON has no syntax for them, so converters usually drop them.
- Namespaces. Prefixes and namespace URIs have no equivalent. They are kept as part of the name, or discarded.
- Element order across different names. An object does not promise an order, so
<a/><b/><a/>loses its interleaving when theaelements are gathered into an array. - Types. Everything is text, so you must decide whether
"3"should become a string or a number.
Going from JSON to XML, the losses are different:
- Arrays. XML has no list. You can repeat an element, but an empty array or a single-item array disappears into nothing or into a lone element.
- Keys that are not valid XML names. A JSON key can be any string, including
"first name"or"2nd", and XML element names cannot. - Top-level values. A JSON document can be an array or a bare number. An XML document needs one root element, so something has to be invented.
- Types.
42and"42"become the same text. - Null and empty string. Both can end up as an empty element.
Each of those has a common workaround. The workarounds are conventions, not standards, and different tools choose differently. That is the first thing to know when you compare outputs from two converters: they can both be right and still disagree.
Mapping conventions in practice
Most converters agree on a core set of rules. These are the ones DevUtilX and the examples in this article follow.
- Elements become object members. The element name is the key, and the content becomes the value.
- Attributes become members with an
@prefix.<item sku="A1">produces"@sku": "A1"inside the item's object. The prefix keeps attributes from colliding with child elements of the same name. - Text next to attributes or children goes under
#text. An element with an attribute and text, such as<item sku="A1">Pen</item>, becomes{"@sku": "A1", "#text": "Pen"}. - Text-only elements collapse to a plain value.
<customer>Ada</customer>becomes"customer": "Ada", with no wrapper object. - Repeated sibling elements become an array. Two
<item>elements under one parent produce"item": [ ... ]. - Empty elements become
null. Some tools use an empty string instead. This is a real choice, and it deserves to be made deliberately. - The root element becomes the single top-level key.
Here is a complete example with the rules applied. The input is XML:
<order id="1001" status="paid">
<customer>Ada Lovelace</customer>
<item sku="A1">Pen</item>
<item sku="B2">Ink</item>
<note/>
<total currency="USD">12.50</total>
</order>
The output below was produced by the Python converter shown later in this article, run in a sandbox:
{
"order": {
"@id": "1001",
"@status": "paid",
"customer": "Ada Lovelace",
"item": [
{ "@sku": "A1", "#text": "Pen" },
{ "@sku": "B2", "#text": "Ink" }
],
"note": null,
"total": { "@currency": "USD", "#text": "12.50" }
}
}
Read it against the rules. The two attributes on order became @id and @status. The two item elements became an array whose entries each carry an attribute and text. The empty note became null. And 12.50 stayed a string, which brings us to types.
Other conventions you will meet
The @ and #text style is common, but not universal. Some tools prefix attributes with an underscore or a dollar sign, or put them in a separate attributes object, and some libraries wrap every child in an array so that the shape never depends on how many elements were present. Pick one convention, document it, and make sure both ends of your integration use it.
Repeated elements and the single-item array problem
This is the most famous trap in XML to JSON conversion, and it is worth understanding properly.
An XML document does not say that an element is a list. It simply repeats the element. A converter that sees two <item> children makes an array. A converter that sees one <item> has no way to know it was meant to be a list, so it produces a single object. The consumer of the JSON then receives an array on some days and an object on others.
The Python converter in this article shows the problem on real input. With two items it produces an array. With one item:
<order><item sku="A1">Pen</item></order>
the default output is
{"order": {"item": {"@sku": "A1", "#text": "Pen"}}}
and consumer code that loops over order.item now fails, or worse, loops over the keys of the object. With the converter told that item is always a list, the output becomes
{"order": {"item": [{"@sku": "A1", "#text": "Pen"}]}}
There are three practical fixes.
- Declare the list elements. Give the converter a set of element names that must always be arrays, as the
force_listandforceArrayparameters do in the code below. This is the usual approach and works well when you know the vocabulary. - Normalise on the consuming side. Wrap the value if it is not already an array. In JavaScript,
[].concat(order.item ?? [])does it in one expression. - Use a schema. If you have an XSD, elements with a maximum occurrence above one are lists by definition. Tools that read the schema can emit arrays reliably.
The opposite direction has the mirror-image problem. A JSON array of one item becomes one element, and an empty array becomes nothing at all. If you then convert that XML back, the array has become an object or a missing key. We will measure this in the round-trip section.
Empty elements, null and missing values
An XML element with no content can be written two ways: <note/> or <note></note>. XML parsers treat the two as identical. Neither one tells you whether the value is null, an empty string, or "this field has no value yet".
JSON, by contrast, has three distinct states: a member with the value null, a member with the value "", and no member at all. Converting from JSON to XML collapses at least the first two into an empty element. Converting back usually yields null or an empty string, depending on the tool, and the original distinction is gone.
XML does have a standard way to say "null": the xsi:nil="true" attribute from the XML Schema instance namespace. A schema can declare that an element is nillable, and a document then marks a specific element as nil. Not every producer or converter understands it, so do not assume it is honoured. If a distinction between null and empty matters in your data, agree on a convention explicitly, for example by omitting the element for null and writing an empty element only for an empty string, then test it with real samples.
Types: XML is all text
When you convert <qty>3</qty> to JSON, you must decide whether the result is "3" or 3. A converter without a schema has only the text to go on. Two policies exist.
Keep everything as strings. This is the safest default. It never changes a value, and it never guesses. The consumer converts to numbers where needed, with knowledge of what each field means.
Infer types from the text. The converter turns 3 into a number and true into a boolean. This is convenient and also dangerous, because text that merely looks like a number may not be one. The JavaScript converter shown later has an infer option. Here is what it does to a sample, as measured by running it:
{"order":{"@id":"1001","qty":"3","paid":"true","zip":"02134"}}
without inference, and
{"order":{"@id":1001,"qty":3,"paid":true,"zip":"02134"}}
with it. The regular expression used for numbers rejects leading zeros, so the zip code 02134 correctly stays a string. That rule is only a heuristic, though. Consider a phone number, a product code such as 1E5, a version string like 1.10 that would become 1.1, or a 20-digit identifier that no JavaScript number can hold exactly. Each of those breaks silently if you infer.
A good rule of thumb is to infer only for fields you know are numeric, or to convert after the fact with a schema in hand. If a document is going to be converted and then converted back, remember that inference also makes the trip lossy: "1.10" and 1.1 produce different XML text.
In the other direction, 42 and "42" both produce <n>42</n>, which is correct behaviour for XML but means the type is lost. Booleans become the strings true and false. Whether a consumer reads them back as booleans depends on the consumer.
Mixed content and element order
This is where simple converters give up, and it is worth seeing exactly how. Run the Python converter on a short HTML-like fragment:
<p>Hello <b>bold</b> world</p>
The output was:
{"p": {"b": "bold", "#text": "Hello"}}
The word world is gone. The converter read the text before the child element, and the text that follows the child (which ElementTree calls the tail) never made it into the object. Even if it had, an object cannot say that Hello comes before the bold word and world after it. The structure is a sequence: text, element, text. JSON expresses a sequence with an array.
Converters that take mixed content seriously use a different shape, an ordered list of children:
{"p": ["Hello ", {"b": "bold"}, " world"]}
This keeps everything, but it is a different convention from the one above, and consumers must handle both strings and objects in the same array. For document-like XML, such as articles, manuals and markup that includes inline formatting, this is the only faithful approach. For data-like XML, such as orders and records, mixed content should not exist, and finding it is usually a sign of a problem in the source.
A related issue is ordering between different element names. XML documents have a definite order: <a/><b/><a/> has three children in that sequence. When a converter groups the two a elements into one array, the interleaving with b is lost. For records with a fixed shape this does not matter. For ordered content, such as steps in a workflow or lines in a form, it matters a great deal. In that case, use the ordered-list shape and keep the sequence explicit.
In the other direction, JSON objects are unordered by specification, though most parsers preserve insertion order in practice. Generating XML from a JSON object therefore produces child elements in whatever order the object holds them. If the receiving system uses an XSD with a sequence that demands a specific order, build the object with the keys in that order, or use a serialiser that takes a schema.
Namespaces
XML namespaces let one document combine vocabularies from different sources without name clashes. A namespace is identified by a URI, and elements and attributes are bound to it through a prefix or a default declaration. The W3C specification, Namespaces in XML, defines the rules.
Here is what a parser sees. In Python's ElementTree, the document
<a xmlns="urn:x" xmlns:p="urn:y"><p:b p:k="1">t</p:b></a>
gives the element names {urn:x}a and {urn:y}b and the attribute name {urn:y}k. The prefix p is not part of the name. The parser resolves it to the URI, and the prefix is only a local abbreviation that could be changed without changing the meaning of the document.
That raises a design question for conversion. You can:
- Keep the prefix in the key (
"p:b"). This is simple, and it preserves how the document looks, but two documents that mean the same thing with different prefixes become different JSON. - Use the full name (
"{urn:y}b"). This is exact but awkward to read and type. - Drop namespaces. This gives clean keys, but two elements that differ only by namespace can collide.
For converting to JSON for a web front end, dropping or simplifying prefixes is often acceptable, as long as you have checked that no collisions occur. For converting back to XML, you must also restore the namespace declarations, typically as @xmlns and @xmlns:p attributes on the root, or the receiver will reject the document. SOAP and many enterprise formats are strict about this, so test against the real receiver and not just against your own parser.
Invalid XML names from JSON keys
XML element names follow strict rules. A name must start with a letter, an underscore or a colon, and the characters that follow may be letters, digits, hyphens, underscores and periods, plus some other Unicode characters. It cannot contain spaces, and names starting with the letters xml in any case are reserved.
JSON keys have no such restriction. "first name", "2nd", "a/b", "@" and the empty string are all legal. So a converter that blindly writes <first name> produces a document that no XML parser will accept.
The Python and JavaScript converters later in this article check each key and raise an error:
ERR invalid XML name: 'first name'
ERR invalid XML name: '1st'
Throwing is the honest choice, because any fix changes your data. Other strategies exist, and all of them are lossy in some way:
- Replace invalid characters with underscores, so that
first namebecomesfirst_name. Two different keys can collapse into one. - Prefix names that start with a digit, such as
_2ndorn2nd. - Move the original key into an attribute, for example
<field name="first name">value</field>. This is lossless, and a common approach for data with arbitrary keys, such as maps keyed by user input.
The last option is worth remembering. When a JSON object is used as a dictionary and its keys are data, not field names, make the key a value in the XML and not a tag name. Keys that come from untrusted input also deserve care for another reason: they will shape the structure of the document you send onward.
The root element requirement
An XML document must have exactly one root element. A JSON document has no such limit: its top-level value can be an object with ten members, an array, a string or a number.
When you convert a JSON object with several top-level keys, there is no single element to be the root. The converters here refuse with a clear message instead of guessing:
ERR need exactly one top-level key to use as the root element
The usual remedy is to wrap the object in a root element of your choosing, such as root or a name that the receiving system expects. For a top-level array, wrap it too: {"items": [1, 2, 3]} produces <items> ... or, more commonly, a root with repeated child elements. Whichever you choose, record the choice. The element name for list members in particular is invented, since JSON arrays do not have one, and a receiver that validates against a schema needs the exact name.
Going the other way, the root element name becomes a top-level key, so the JSON always has an extra level of nesting that a handwritten JSON API would not have. Many teams strip it when converting XML responses for a front end. If you do so, you take on the task of putting it back, correctly, when you convert the other way.
Round-trip testing
If a conversion pair is supposed to be reversible, test the claim. A round trip converts A to B and back, and compares the result with A. Differences show exactly which constructs your conventions cannot preserve.
Here is the result of running small JSON documents through the JavaScript converter and back, with type inference on:
| Original JSON | XML produced | JSON after round trip |
|---|---|---|
{"r":{"item":[1]}} |
<r><item>1</item></r> |
{"r":{"item":1}} |
{"r":{"item":[]}} |
<r/> |
{"r":null} |
{"r":{"a":null,"b":""}} |
<r><a/><b></b></r> |
{"r":{"a":null,"b":null}} |
{"r":{"zip":"02134"}} |
<r><zip>02134</zip></r> |
{"r":{"zip":"02134"}} |
The first row shows the single-item array collapsing into a scalar. The second shows an empty array vanishing, and in fact taking the whole item key with it. The third shows null and the empty string becoming indistinguishable. The fourth is the one that survived, because the inference rule refused the leading zero.
Going the other way, XML to JSON and back, the lost items are different: comments, attribute-versus-element choices, namespace prefixes, whitespace between elements, and the order of interleaved siblings. Two XML documents can be equal for an application and still differ as text, so compare them by parsing both and comparing the trees, not by comparing strings. Our guide to comparing files and diffs covers the text-level tools.
A good test suite for a converter has at least these cases:
- A single repeated element, one occurrence and several.
- An empty element and an element containing only whitespace.
- An element with both attributes and text.
- A text value that looks like a number, a boolean and a date.
- Special characters:
<,&, quotes, and non-ASCII text. - Namespaces, if the real data has them.
- A key that is not a valid XML name.
- A JSON array at the top level.
Keep real samples from production, with sensitive values removed, in the test suite. Invented samples tend to be cleaner than the data you will actually receive.
Schemas: XSD and JSON Schema
Both ecosystems have a way to describe the allowed shape of a document, and they matter for conversion because schemas are the only reliable source of the information a converter lacks.
An XML Schema Definition (XSD) describes elements, attributes, their order, the number of times they may repeat and their data types, such as integer, decimal, date and boolean. An XSD turns the guessing problems of this article into facts: it says whether item is a list, whether qty is an integer and whether note may be nil. XSD also supports ordering constraints, key and reference constraints, and type derivation.
JSON Schema describes JSON documents. It can state that a member is a string, a number or an array, constrain ranges and patterns, mark members as required and describe the items of an array. It does not need to deal with attributes, mixed content or namespaces, since JSON has none.
At a high level the two overlap on types, required fields and repetition, and differ in what each cannot express. Neither maps onto the other perfectly. If you convert documents between formats regularly, there are two sound strategies:
- Treat one schema as the source of truth and generate the other, or at least the converter configuration, from it. Know which constructs will not survive.
- Validate on both sides. Validate the XML against the XSD before converting, and the JSON against its schema afterwards. A converter bug that produced the wrong shape then fails in a test and not in production.
Schema validation catches the structural mistakes. It does not replace reading the sample outputs, so do both.
Real-world use cases
Integrating with SOAP and legacy web services. SOAP messages are XML envelopes with a header and a body, usually with namespaces. A common pattern is an adapter that accepts JSON from a modern client, builds the SOAP XML, calls the legacy service and converts the XML reply back to JSON. The risk is in the details: namespace declarations, element order required by the schema, and fields that must be arrays even when they hold one item.
Reading RSS and Atom feeds. Feeds are XML, and web clients often want them as JSON. Feeds are a good example of data with attributes (Atom's link elements carry href and rel as attributes), repeated elements (entries) and namespaces (extensions such as media or iTunes tags). Converting a feed to JSON works well if you keep the @ attributes, force entry and item to arrays and handle the case of a feed with a single entry.
Configuration files. Build tools, application servers and IDEs have long used XML for configuration, while newer tools prefer JSON or YAML. Converting a configuration file once, as a migration, is a very good use for a converter. Review the result by hand: comments carry explanations that conversion drops, and attribute-heavy files produce noisy @ keys that you may want to restructure.
Data exchange with partners. Banks, government agencies, logistics firms and healthcare systems often publish XML interfaces. Teams convert on ingestion so that internal services can work with one format. Keep the original XML alongside the converted JSON when auditability matters, since the conversion drops information.
Testing and fixtures. Developers often keep a sample XML response and want a JSON equivalent for a mock server, or the reverse. A quick converter saves hand-writing both.
Debugging and exploration. XML is verbose, and a flattened JSON view makes it easier to see the shape of a large document. The conversion is for reading, so its losses do not matter. Our XML formatting guide covers the readable layout of the XML itself.
Vector graphics and markup documents. SVG is XML, and converting it to JSON is occasionally useful for programmatic editing, but it is a document format with mixed content and namespaces, so use an XML-aware library for serious work. See our SVG formatting guide.
Step-by-step: convert with DevUtilX
For one-off jobs, a browser tool is quicker than writing code. DevUtilX has two converters, and both run entirely in your browser, so your data is processed locally and is not uploaded.
From XML to JSON
- Open the XML to JSON tool.
- Paste your XML into the input editor. Make sure it is well-formed: one root element, every tag closed, attribute values in quotes.
- Run the conversion.
- Read the output with the mapping rules above in mind. Look for the
@keys (attributes),#textkeys (text next to attributes or children) and arrays (repeated elements). - Check any element that appeared only once in your sample, since it could be a list that happened to hold one item.
- Copy the JSON to where you need it.
From JSON to XML
- Open the JSON to XML tool.
- Paste your JSON. If it has more than one top-level key, wrap it in a single root key first, since XML needs one root element.
- Check that the keys are valid XML names. Spaces and leading digits will cause an error.
- Use
@-prefixed keys for members that should become attributes and#textfor element text next to attributes. - Run the conversion and copy the XML.
If either tool reports an error, the input is not well-formed. For XML, the usual causes are an unclosed tag, an unescaped ampersand, mismatched tag case or a second root element. For JSON, look for trailing commas, single quotes or unquoted keys. You can then use the XML formatter guidance and the JSON formatting guide to tidy the input. Even with local processing, do not paste credentials or production secrets into any tool when a redacted sample would do.
Converters in code
Tools are fine for one-offs. Production conversions need code you can test, version and tune. These converters were run in the sandbox, and the outputs shown in this article come from them.
Python: XML to JSON
Python's standard library includes xml.etree.ElementTree, which is enough for this.
import json
import xml.etree.ElementTree as ET
def convert(el, force_list=()):
node = {}
for k, v in el.attrib.items():
node["@" + k] = v
for child in el:
value = convert(child, force_list)
key = child.tag
if key in node:
if not isinstance(node[key], list):
node[key] = [node[key]]
node[key].append(value)
elif key in force_list:
node[key] = [value]
else:
node[key] = value
text = (el.text or "").strip()
if not node:
return text if text else None
if text:
node["#text"] = text
return node
def xml_to_json(xml_string, force_list=()):
root = ET.fromstring(xml_string)
return {root.tag: convert(root, force_list)}
Walk through the logic. Attributes come first and get the @ prefix. Each child is converted recursively. If a key is already present, the existing value is promoted to a list and the new one appended, which is the rule for repeated elements. If the key is in force_list and is not yet present, the first value goes straight into a list. Finally the element's own text is stripped, and if the node has no attributes or children, the function returns the text itself, or None for an empty element.
As the earlier outputs showed, this version drops the tail text in mixed content, and {urn:x}a style names appear when namespaces are used. Both are decisions you would revisit for real data.
Python: JSON to XML
import re
import xml.etree.ElementTree as ET
NAME = re.compile(r"^[A-Za-z_][\w.\-]*$")
def build(parent, tag, value):
if not NAME.match(tag):
raise ValueError("invalid XML name: %r" % tag)
el = ET.Element(tag) if parent is None else ET.SubElement(parent, tag)
fill(el, value)
return el
def fill(el, value):
if isinstance(value, dict):
for k, v in value.items():
if k.startswith("@"):
el.set(k[1:], str(v))
elif k == "#text":
el.text = str(v)
elif isinstance(v, list):
for item in v:
build(el, k, item)
else:
build(el, k, v)
elif value is None:
pass
elif isinstance(value, bool):
el.text = "true" if value else "false"
else:
el.text = str(value)
def json_to_xml(obj):
if not isinstance(obj, dict) or len(obj) != 1:
raise ValueError("need exactly one top-level key to use as the root element")
(tag, value), = obj.items()
if isinstance(value, list):
raise ValueError("root value cannot be an array")
return ET.tostring(build(None, tag, value), encoding="unicode")
Note the check for booleans before the generic case. In Python, bool is a subclass of int, but the order here is about the output text: without the special case you would get True with a capital letter, which is not what most XML consumers expect. Run on this input:
doc = {"order": {"@id": "1001", "customer": "Ada Lovelace",
"item": [{"@sku": "A1", "#text": "Pen"}, {"@sku": "B2", "#text": "Ink"}],
"note": None, "paid": True, "total": {"@currency": "USD", "#text": 12.5}}}
print(json_to_xml(doc))
the output was:
<order id="1001"><customer>Ada Lovelace</customer><item sku="A1">Pen</item><item sku="B2">Ink</item><note /><paid>true</paid><total currency="USD">12.5</total></order>
The values 5 < 6 & 7 are escaped by ElementTree automatically. The sandbox run of {"a": {"x": "5 < 6 & 7"}} gave <a><x>5 < 6 & 7</x></a>. Escaping is one of the best reasons to use a serialiser and never build XML by concatenating strings.
JavaScript: both directions, no dependencies
In the browser, you would usually reach for the built-in DOMParser, documented on MDN, to parse XML. Node.js has no built-in XML parser, so the version below uses a small reader that handles elements, quoted attributes, text and the five predefined entities. It does not handle CDATA, comments, DOCTYPE declarations or namespaces, so treat it as a teaching example and a starting point, and use a real parser for untrusted or complex input.
const NAME = /^[A-Za-z_][\w.\-]*$/;
const esc = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
const escAttr = (s) => esc(s).replace(/"/g, """);
function toXml(tag, value) {
if (!NAME.test(tag)) throw new Error("Invalid XML name: " + tag);
if (Array.isArray(value)) return value.map((v) => toXml(tag, v)).join("");
if (value === null || value === undefined) return `<${tag}/>`;
if (typeof value !== "object") return `<${tag}>${esc(value)}</${tag}>`;
let attrs = "", body = "";
for (const [k, v] of Object.entries(value)) {
if (k.startsWith("@")) attrs += ` ${k.slice(1)}="${escAttr(v)}"`;
else if (k === "#text") body += esc(v);
else body += toXml(k, v);
}
return body === "" ? `<${tag}${attrs}/>` : `<${tag}${attrs}>${body}</${tag}>`;
}
function jsonToXml(obj) {
const keys = Object.keys(obj);
if (keys.length !== 1 || Array.isArray(obj[keys[0]]))
throw new Error("Need exactly one root key with a non-array value");
return toXml(keys[0], obj[keys[0]]);
}
function xmlToJson(xml, { infer = false, forceArray = [] } = {}) {
const re = /<(\/?)([A-Za-z_][\w.\-:]*)((?:\s+[\w.\-:]+\s*=\s*"[^"]*")*)\s*(\/?)>|([^<]+)/g;
const decode = (s) => s.replace(/</g, "<").replace(/>/g, ">")
.replace(/"/g, '"').replace(/&/g, "&");
const scalar = (s) => {
if (!infer) return s;
if (s === "true") return true;
if (s === "false") return false;
if (/^-?(0|[1-9]\d*)(\.\d+)?$/.test(s)) return Number(s);
return s;
};
const stack = [{ tag: "#root", node: {}, text: "" }];
let m;
while ((m = re.exec(xml))) {
const top = stack[stack.length - 1];
if (m[5] !== undefined) { top.text += decode(m[5]); continue; }
const [, close, tag, rawAttrs, selfClose] = m;
if (!close) {
const node = {};
for (const a of rawAttrs.matchAll(/([\w.\-:]+)\s*=\s*"([^"]*)"/g))
node["@" + a[1]] = scalar(decode(a[2]));
stack.push({ tag, node, text: "" });
}
if (close || selfClose) {
const done = stack.pop();
const text = done.text.trim();
const keys = Object.keys(done.node);
const value = keys.length ? done.node : (text ? scalar(text) : null);
if (keys.length && text) done.node["#text"] = scalar(text);
const parent = stack[stack.length - 1].node;
if (done.tag in parent) {
if (!Array.isArray(parent[done.tag])) parent[done.tag] = [parent[done.tag]];
parent[done.tag].push(value);
} else parent[done.tag] = forceArray.includes(done.tag) ? [value] : value;
}
}
return stack[0].node;
}
With this sample:
const xml = `<order id="1001"><customer>Ada Lovelace</customer><item sku="A1">Pen</item><item sku="B2">Ink</item><qty>3</qty><paid>true</paid><zip>02134</zip><note/></order>`;
console.log(JSON.stringify(xmlToJson(xml)));
console.log(JSON.stringify(xmlToJson(xml, { infer: true })));
the sandbox run printed:
{"order":{"@id":"1001","customer":"Ada Lovelace","item":[{"@sku":"A1","#text":"Pen"},{"@sku":"B2","#text":"Ink"}],"qty":"3","paid":"true","zip":"02134","note":null}}
{"order":{"@id":1001,"customer":"Ada Lovelace","item":[{"@sku":"A1","#text":"Pen"},{"@sku":"B2","#text":"Ink"}],"qty":3,"paid":true,"zip":"02134","note":null}}
Look at the second line: with inference on, even the attribute id became the number 1001. That may be what you want, or it may not, which is the argument for inferring types on named fields and not globally. And converting { order: { customer: "Ada & Co", ... } } with jsonToXml gave <customer>Ada & Co</customer>, which the reader decoded back to Ada & Co.
In a browser, the XML to JSON direction is shorter and more robust if you walk the DOM that DOMParser returns. The parseFromString(text, "application/xml") call reports errors by returning a document containing a parsererror element, so check for that before reading.
Security: parsing XML safely
Parsing JSON can go wrong, but parsing XML has a particular hazard, and it is the strongest argument for care when you accept XML from outside.
The XML standard allows a document to declare entities in a DOCTYPE. An entity is a named piece of replacement text, and an external entity can point at a file path or a URL. A parser that resolves external entities can be tricked into reading a local file or making a network request on behalf of an attacker, and returning or leaking the content. This is the XML External Entity attack, usually written XXE. A related problem is entity expansion: a small document that defines entities in terms of other entities can grow to an enormous size when expanded, exhausting memory. The OWASP XXE prevention cheat sheet lists the safe settings for many languages.
Practical rules:
- Disable DTD processing and external entity resolution in whichever parser you use. Many modern parsers do this by default, but check the documentation for your library and version rather than assuming.
- Reject documents with a DOCTYPE if you do not need one. Data interchange formats almost never do.
- Cap the input size before parsing, and cap nesting depth where the parser lets you.
- Do not build XML with string concatenation. Use a serialiser so that special characters are escaped. Concatenating user input into XML is injection, in the same family as SQL injection.
- Validate the result against a schema where one exists, and treat parsed data as untrusted.
- Check what your language documents. Python's own documentation has a section on XML vulnerabilities and notes which of the standard modules are affected by which kind of attack. In a quick sandbox test, a document with an internal entity definition was expanded by ElementTree without complaint, which shows why input limits matter even when external fetching is not a risk.
On the JSON side, the matching concerns are different: large inputs, deep nesting and unvalidated data. Converting from JSON to XML adds one more: keys and values from untrusted JSON become tag names, attributes and text in a document that another system will parse. Validate key names, escape values and do not let user input choose the root or the namespaces. Our JSON minification guide also covers request-size limits and parsing cost.
The browser tools process input locally, which avoids uploading it. That is not a replacement for safe parsing in your own services.
Performance considerations
XML is more verbose than JSON for the same data, because each element name appears twice, in the opening and closing tag. For a record with short values the XML is commonly noticeably larger than the equivalent compact JSON, though compression narrows the gap since the repeated tag names compress well. The same advice as in the payload size guide applies: compress at the server, and measure real payloads before deciding.
For conversion code, a few points matter:
- Streaming. A tree-based parser holds the whole document in memory, and the converted object adds a second copy. For multi-gigabyte files use a streaming parser, such as SAX or a pull parser, and convert record by record. Writing the output as newline-delimited JSON fits that pattern.
- Repeated work. If you convert the same feed repeatedly, cache by ETag or hash, and skip the conversion when nothing changed.
- Server-side versus client-side. Parsing a large XML document in a browser main thread can freeze the page. Convert on the server, or in a Web Worker.
- Number handling. Type inference runs a regular expression per text node, which is cheap but not free on huge documents. Converting only the needed fields is faster than testing everything.
Accessibility and usability considerations
Data conversion rarely touches accessibility directly, but a few points are worth a mention. If you use a converted document to render content on a page, such as a feed rendered from XML, keep semantic structure: the converter should preserve headings, links and alt text attributes, which means keeping the @ attributes your markup depends on. Dropping attributes in the name of simple JSON can strip lang, alt or href values that assistive technology relies on. For mixed-content documents, the ordered-list shape preserves reading order, and a flattened object does not, so a screen reader would hear the words in the wrong order. Pair converted content with the checks in our HTML formatting and semantic markup guide.
Within the tools themselves, label the input and output regions, support keyboard operation and make errors available as text and not colour alone. DevUtilX follows those practices for its editors.
Common mistakes
- Treating conversion as reversible. It is not. Test your round trip, and keep the original when the details matter.
- Not handling the single-item list. The most common production bug in XML to JSON pipelines. Declare list elements.
- Inferring types everywhere. Zip codes, phone numbers, version strings and long identifiers get corrupted. Infer selectively.
- Using invalid names as tags. Spaces, leading digits and symbols in JSON keys produce broken XML.
- Forgetting the root. A JSON object with several top-level keys needs a wrapper element.
- Dropping namespaces without checking for collisions. Two elements with the same local name in different namespaces will overwrite or merge each other.
- Ignoring mixed content. A converter that drops text after a child element corrupts documents silently. Check for mixed content in document-like XML.
- Building XML by string concatenation. Unescaped ampersands and angle brackets will break the document, and attacker-controlled values can alter its structure.
- Parsing untrusted XML with defaults you have not checked. XXE and entity expansion are real. Configure the parser.
- Comparing XML as text. Attribute order, whitespace and prefix choices differ without changing meaning. Compare parsed trees.
- Assuming object key order in XML output. If a schema demands an order, enforce it explicitly.
- Trusting one converter's output as the standard. Another tool's different but consistent convention is not an error. Agree on the convention across the integration.
Troubleshooting guide
When a conversion does not behave, work from symptom to cause.
Symptom: the converter reports a parse error on XML input
Cause. The XML is not well-formed. The usual culprits are an unclosed tag, tag names whose case differs between open and close, an unescaped & or < in text, an attribute without quotes, or two root elements.
Fix. Read the error position, if given, and fix the first problem, since later errors often follow from it. Escape & as &, and check that the document has a single root.
Symptom: a field is an array for some documents and an object for others
Cause. The element repeats in some documents and appears once in others, and the converter infers lists from repetition.
Fix. Declare the element as always a list (force_list or forceArray), or normalise it in the consumer.
Symptom: text after a child element is missing
Cause. Mixed content. The converter kept the text before the first child and dropped the tail.
Fix. Use a converter with an ordered-children mode, or process that element with an XML-aware library. If the XML is data and not a document, ask the source why it contains mixed content.
Symptom: null appears where you expected an empty string
Cause. The converter maps empty elements to null.
Fix. Configure the empty-element policy if the tool allows it, or coerce in the consumer. Agree on the meaning of an empty element with the data owner.
Symptom: a value that was a string became a number, or lost its leading zero
Cause. Type inference parsed text like 02134 or 1.10 as a number.
Fix. Turn off global inference and convert only the fields you know are numeric, or use a schema. Send identifiers as strings.
Symptom: JSON to XML fails with an invalid name error
Cause. A key contains a space, starts with a digit or has a character that XML names do not allow.
Fix. Rename the key, sanitise it with a documented rule, or restructure the data so the key becomes a value, as in <field name="first name">.
Symptom: the receiving system rejects the generated XML although it is well-formed
Cause. Well-formed is not valid. The receiver checks a schema that requires a specific element order, namespaces, a particular root element or typed values.
Fix. Validate the output against the receiver's XSD. Build keys in the required order, add namespace declarations through @xmlns attributes and use the exact element names the schema expects.
Symptom: the prefix ns1: or curly-brace names appear in the JSON
Cause. The parser reports namespaced names in expanded form or the converter keeps prefixes.
Fix. Choose a policy: strip prefixes after confirming there are no collisions, or keep them and document it. Restore declarations when converting back.
Symptom: special characters look wrong, such as é
Cause. A character encoding mismatch: bytes in UTF-8 were decoded as another encoding, or the reverse.
Fix. Give the parser raw bytes so that it can honour the XML declaration, and write JSON as UTF-8.
Symptom: conversion of a big file runs out of memory
Cause. The whole document is held as a tree, and the converted structure is held as well.
Fix. Use a streaming parser and write records out as you go.
When to choose JSON and when to choose XML
Conversion is often a symptom of a choice made long ago. For a new project, think about what the data is.
Choose JSON for web and mobile APIs, for records and lists, for data that maps onto the objects and arrays of your programming language, and when you want a compact, fast-to-parse format with native types. Most client libraries and browsers handle it natively.
Choose XML for document-like content with mixed text and markup, when you need namespaces to combine vocabularies, when a standard that you must follow is defined in XML (SOAP, many industry and government formats, feeds, office and graphics formats), or when you rely on its mature toolchain: XSD validation, XSLT transformations and XPath queries.
Do not choose by fashion. A team that already has XSDs, validators and partners on XML does not gain much by moving a stable interface to JSON, and a team building a public web API that returns XML only is making its consumers' lives harder. Where both sides must be supported, pick one as the canonical internal model, convert at the edges and test the conversions.
Comparison: formats and approaches
| Format or approach | Strengths | Weaknesses | Best for |
|---|---|---|---|
| JSON | Compact, native types, simple, ubiquitous in web APIs | No comments, no attributes, no mixed content | APIs, app data, configuration |
| XML | Namespaces, mixed content, attributes, mature schema and transform tooling | Verbose, text-only values, heavier parsing, XXE risk | Documents, SOAP, feeds, standards-bound exchange |
| YAML | Readable, supports comments | Subtle syntax rules, many ways to write the same thing | Hand-edited configuration |
| Protocol Buffers and other binary formats | Compact and fast, strong schemas | Not human readable, tooling required | High-volume service-to-service traffic |
Convention-based conversion (@, #text) |
Quick, no schema needed | Ambiguity, lossy, depends on conventions | Reading, migration, simple adapters |
| Schema-driven conversion (from an XSD) | Reliable lists and types | Needs a schema and more setup | Long-lived integrations |
| Ordered-children representation | Preserves mixed content and order | More complex to consume | Document-style XML |
| XSLT and XPath | Transform and query XML without leaving it | A separate skill, XML on both ends | XML to XML or XML to HTML pipelines |
The practical lesson of the table is that conversion by convention suits data, schema-driven conversion suits integrations that must not break, and an ordered representation is needed whenever text and markup mix.
FAQ
Is JSON better than XML?
Neither is better in general. JSON is lighter and fits application data. XML is richer for documents and standards-driven exchange. Choose based on the data and on what your partners and tools support.
Can XML always be converted to JSON?
You can always produce some JSON, but not always without loss. Attributes, comments, namespaces, mixed content and interleaved ordering need conventions or are dropped.
Can JSON always be converted to XML?
Not directly. Keys must be valid XML names, the document needs one root element, and arrays and nulls need conventions.
What do @ and #text mean in converted JSON?
They are a widespread convention. A key starting with @ represents an XML attribute, and #text holds an element's text when it also has attributes or children.
Why did my single <item> become an object instead of an array?
XML does not mark lists. A converter sees one element and produces one object. Declare the element as always a list, or wrap the value in the consuming code.
How is null represented in XML?
There is no universal form. Common choices are an empty element, an omitted element, or xsi:nil="true" when a schema defines it. Agree on one with your counterpart.
Why are all my numbers strings after converting from XML?
XML values are text. Safe converters keep them as strings. You can turn on inference, or convert named fields in your code, but be careful with zip codes, identifiers and versions.
Do comments survive the conversion?
Almost never. JSON has no comments, so they are dropped. Keep the original file if the comments matter.
What happens to XML namespaces?
It depends on the tool. Prefixes may be kept in key names, expanded to full URIs or removed. If you convert back, restore the declarations, or the receiver may reject the document.
What is XXE and why does it matter for conversion?
XML External Entity attacks abuse entity declarations in a DOCTYPE to read files or make requests. If you convert XML from outside sources, configure the parser to disable DTDs and external entities.
Is converting in the browser safe?
DevUtilX tools process your input locally in the browser, so nothing is uploaded. As always, avoid pasting secrets into any tool.
Will converting there and back give me the original?
Not guaranteed. Single-item arrays, empty arrays, null versus empty text, attribute placement and order can all change. Test round trips on real samples.
How should I handle mixed content such as <p>Hello <b>bold</b> world</p>?
Use a converter with an ordered-children representation, or process the document with an XML library. A plain object mapping drops or reorders text.
Which is smaller on the wire, JSON or XML?
For the same data, compact JSON is typically smaller because XML repeats each element name in the closing tag. Compression reduces the difference. Measure your own payloads.
Conclusion
Converting between JSON and XML is easy to start and hard to finish well. The formats model data differently: attributes, mixed content, namespaces and ordering exist only in XML, while arrays and native types exist only in JSON. A reliable conversion rests on explicit conventions, declared list elements, careful type handling, validation of names and roots, round-trip tests on real samples and a safely configured XML parser. When you only need a quick result, open the XML to JSON or JSON to XML converter and check the output against the rules in this guide. To keep learning, read our guides on XML formatting and structure and JSON formatting, and see how size trade-offs play out in SVG and XML minification.
Further reading
Try the tools
Seamlessly convert your JSON data into a well-structured XML format. This tool is perfect for data transformation tasks, ensuring compatibility between systems that use different data formats for their operations.
Effortlessly convert your XML data into a clean and easy-to-handle JSON format. This tool is perfect for data transformation and for use with modern web APIs, ensuring seamless integration with other systems.


