JSON has arrays. XML has repetition.
This is the one real decision in the whole conversion. JSON marks a list with brackets, so "tags": ["new", "sale"] says plainly that two values belong to one key. XML has no bracket. A list is written by repeating an element name and hoping the reader notices.
<tags>new</tags><tags>sale</tags><tags><item>new</item><item>sale</item></tags>Repeating the key is what most XML schemas expect, and what an XSD with maxOccurs="unbounded" describes. Wrapping is easier to read and survives a trip back to JSON without guesswork, because the wrapper element proves a list existed even when only one entry is left in it. Pick wrapping when the XML feeds something you control. Pick repetition when a schema on the other end already decided for you.
Not every JSON key is a legal XML tag
JSON object keys are strings, so anything goes. "order id", "2024_total", and "user.name" are all fine. XML element names follow a grammar: start with a letter or an underscore, then letters, digits, hyphens, periods, and underscores. Spaces are out. A leading digit is out. Names beginning with the three letters xml in any casing are reserved by the specification.
Rather than failing on those keys, this page rewrites them and counts each change in the Keys renamed tile above. The rules are fixed so the same input always produces the same output:
| JSON key | XML element | Why it moved |
|---|---|---|
"order id" | <order_id> | Whitespace inside a name ends the name, so a space would split the tag in two. |
"2024_total" | <_2024_total> | A name starting with a digit is invalid, so an underscore goes in front of it. |
"xmlVersion" | <_xmlVersion> | The xml prefix is reserved in every casing, including XmL. |
"price($)" | <price___> | Punctuation outside the allowed set is replaced character for character, keeping length stable. |
"user.name" | <user.name> | Periods are legal in XML names, so this one passes through untouched. |
"" | <_> | An empty key has no valid form, so it becomes a bare underscore. |
Renaming is one way. Nothing reverses order_id back into "order id" later, which matters if the XML is meant to round trip. Load the Keys XML rejects sample to see all six rules fire at once.
Attributes are a decision your JSON never made
XML stores data in two places. Element content sits between tags. Attributes sit inside the opening tag. JSON has only keys and values, so a converter has no way of knowing which of your fields the receiving system expects as an attribute.
The common answer, used by Badgerfish, by the Python xmltodict library, and by most XML to JSON tools in the other direction, is a naming convention. Prefix a key with @ and it becomes an attribute. Name a key #text and it becomes the element body.
{"book": {"@isbn": "978-0","@lang": "en","#text": "Hard Times"}}<book isbn="978-0" lang="en">Hard Times</book>Untick @keys become attributes and the same input produces <_isbn> and <_text> child elements instead, because @ and # are not valid name characters. That is the right setting when your JSON legitimately contains keys starting with @, which happens with JSON-LD payloads full of @context and @type.
An attribute value has to be a single string. Point @meta at an object or an array and the conversion writes it as a child element instead, because there is no correct flattening. Attributes also hold no order and no repetition, so two identical attribute names on one element are a hard XML error rather than a list.
Types do not survive the crossing
JSON distinguishes a number from a string from a boolean from null. XML text content is text. Once "stock": 0 becomes <stock>0</stock>, nothing in the document says whether that zero is a number, a string, or a code. A schema restores the meaning. Without one, the parser on the far side guesses.
- Numbers print exactly as JSON holds them, so
1.50arrives as1.5and large integers past 2^53 have already lost precision before this page sees them. - Booleans print as
trueandfalse. XSD boolean accepts those, along with1and0, so most validators are happy. - null becomes an empty self-closing element such as
<note/>. An empty string produces the same thing, which means the difference between "no value" and "blank value" is gone. - An empty array in repeat mode writes nothing at all, since zero repetitions of a tag is zero tags. Switch to wrapper mode and you get
<tags></tags>, which at least records that the key was there. - The five reserved characters are escaped in the output. Ampersand, less than, and greater than in text. Those plus the double quote inside attribute values. Everything else passes through as UTF-8, so accented characters and emoji stay readable rather than turning into numeric entities.
A round trip will not give you your JSON back. Convert here, then run the result through an XML to JSON tool, and the differences show up immediately. Arrays with one element read as a plain object. Numbers come back as strings. Nulls and empty strings are indistinguishable. Renamed keys stay renamed. If the goal is to hand data to a system that speaks XML, this is fine, because that system has its own schema. If the goal is to store JSON as XML and read it back later, the conversion is lossy in five separate ways and you are better off storing the JSON.
Reading the counters
The five tiles under the panes describe the document you are about to save, and two of them catch problems early.
Max depth counts nesting levels from the root element down. XML parsers with entity expansion limits, SOAP stacks, and older enterprise integrations often cap nesting around 30 to 50 levels. Deeply nested JSON from a GraphQL response or a nested aggregation query hits those limits faster than people expect.
Size change compares raw bytes against the JSON you pasted. Expect growth of 30 percent to well over 100 percent, because every value carries a closing tag that JSON writes as a single comma. A key like "description" costs eleven characters in JSON and twenty six in XML. Moving fields into attributes cuts that back, which is the practical reason attribute style exists. Compression flattens most of the difference on the wire, so treat the percentage as a file size figure rather than a bandwidth one.
Where this page stops
Namespaces are not generated. If your target schema needs xmlns:ns declarations and prefixed element names, add them by hand or through the transform step downstream. A key written as "ns:price" keeps its colon only when the target parser is not namespace aware, so this page replaces it with an underscore rather than emit a prefix bound to nothing.
There is no CDATA option. Text is escaped instead, which is equivalent for any parser and avoids the nesting problem CDATA sections have with the ]]> sequence. There is no schema generation either. If you need an XSD, generate it from the finished XML with a dedicated tool.
Everything runs in this tab. The JSON never leaves your browser, which is what makes the page usable with a production payload full of customer records. Nothing persists after you close the tab, so save the output you want to keep.
Other pages for the neighbouring jobs
Going the other direction, from XML back to JSON, is handled by the XML converter. If the XML you produce here needs re-indenting after a downstream step mangles it, the XML pretty print page handles that. If the JSON refuses to parse before you get this far, run it through the JSON fixer first, since this page reports the error position but leaves the repair to you. And when the target is a spreadsheet rather than a document, JSON to CSV skips the tag overhead entirely.
