Converters

XML to JSON

Convert XML data to JSON format.

Input XML
JSON result

XML to JSON Converter

Converts XML data to JSON format. Useful for modern API integration and JavaScript processing.

Features

  • Automatic structure parsing
  • Attribute handling
  • Formatted JSON output
  • Array detection

Use Cases

  • REST API integration
  • Frontend data processing
  • Configuration migration

Parses an XML document with fast-xml-parser and prints indented JSON: elements become keys, attributes become @_-prefixed keys, repeated siblings become arrays. It suits turning SOAP or RSS responses, Android manifest files, and legacy XML configuration exports into objects you can read in JavaScript.

One element, three possible shapes

Shapes come from context, not from a schema. An element holding only text collapses to a scalar, so <customer>Ada</customer> yields "customer": "Ada". Add an attribute or a child element and the value turns into an object: attributes are keyed @_id, and stray text moves to #text, a key that appears only when the element also has children or attributes. Inside that object the child keys come first, then #text, then the attribute keys, which is why the @_ lines always sit at the bottom. Siblings sharing a name are collected into an array while a lone occurrence stays unwrapped, and an empty element becomes "". Mixed content loses inline order: <p>Hello <b>world</b>!</p> gives {"b": "world", "#text": "Hello!"}, the two text runs concatenated around the child.

Numbers are inferred, then rewritten

The component enables parseAttributeValue and leaves parseTagValue at its default, so both attribute values and text nodes pass through strnum. Anything shaped like a number becomes a JS number: qty="2" gives 2, 99.50 gives 99.5, 0x1F gives 31, 1e5 gives 100000, and lower-case true gives a boolean. Leading zeros vanish with the type: code="007" becomes 7 and a postal code 01234 becomes 1234, which quietly damages invoice numbers and zero-padded IDs. Twenty-digit integers survive as strings, but the 2^53 boundary is uneven — 9007199254740991 and 9007199254740992 arrive as numbers while -9007199254740993 stays a string. No control in the toolbar turns the inference off, so types must be restored on the JSON side.

It parses, it never validates

The component calls XMLParser, not XMLValidator, inside a single try/catch, so the red banner appears for very few inputs. A mismatched pair such as <a><b></a> returns {"a":{"b":""}} instead of an error, a bare ampersand inside text is swallowed unchanged, and a tag name starting with a digit is accepted as a key. What does throw is a broken processing instruction, reported as Pi Tag is not closed. The practical consequence: a truncated RSS feed or a half-copied config file converts into a plausible object full of empty strings, so check the source with a validator before trusting the JSON. The declaration is not dropped either — <?xml version="1.0"?> turns into a ?xml key whose version is the number 1, and comments simply disappear.

Worked example

An order with attributes, repeated lines and padded values

Input
<order id="A-1007">
  <customer>Ada</customer>
  <total currency="USD">99.50</total>
  <line qty="2">Widget</line>
  <line qty="1">Gizmo</line>
</order>
Output
{
  "order": {
    "customer": "Ada",
    "total": {
      "#text": 99.5,
      "@_currency": "USD"
    },
    "line": [
      {
        "#text": "Widget",
        "@_qty": 2
      },
      {
        "#text": "Gizmo",
        "@_qty": 1
      }
    ],
    "@_id": "A-1007"
  }
}

The two line elements collapse into an array while customer appears once and stays a bare string. total is the only element with both text and an attribute, so it is the only one that gets a #text key, and the @_currency entry is written after the children. Numeric-looking values are rewritten — 99.50 loses its trailing zero, qty="2" becomes the number 2 — whereas A-1007 is not numeric and keeps its string type. The result is 278 characters at the default 2-space indent, 374 at 4 spaces.

Limits and pitfalls

  • Comments are discarded and CDATA sections are unwrapped into plain text, so notes and raw payloads stored in either construct do not survive the conversion.
  • The <?xml ...?> declaration becomes a top-level ?xml key with a numeric version, so code that iterates keys will see an extra entry that has to be deleted first.
  • Array versus object is decided per document: an element occurring once is a string or object, a repeated one is an array, so two responses of the same schema can produce two different JSON types.
  • Numeric inference has no toolbar switch, so zero-padded identifiers and decimal formatting such as 0.10 are rewritten to numbers and cannot be restored from the output.
  • There is no input size cap and no worker: the whole document is re-parsed and re-stringified synchronously on every keystroke, so multi-megabyte exports lock up the editor.

Related tools

When it goes wrong

The JSON came out longer than the XML I pasted, and half of it is @_ lines

CauseEvery attribute becomes its own key with the @_ prefix, and the object is printed one property per line, so <line qty="2"/> — 15 characters — occupies three lines: "line": {, "@_qty": 2, }. The built-in sample behaves the same way: 211 bytes of XML become 275 bytes over 19 lines at 2 spaces, 365 bytes at 4.

FixDrop the indent from 4 back to 2 — those are the only two widths in the toolbar and there is no minify option — then compact what you copied outside the page with jq -c .. If a small file is the real goal, skip the page and pipe the XML through yq -p xml -o json -I0.

After converting a SOAP response, data.Envelope is undefined

CauseThe component never sets removeNSPrefix, so qualified names keep their colon: the root key is "soap:Envelope", the body is "soap:Body", and namespace declarations come through as @_xmlns:soap. A colon can never appear in a dot-path.

FixRead the object with brackets (data['soap:Envelope']['soap:Body']) and strip the prefix in your accessor, or re-run the parse in a short script with removeNSPrefix: true, which removes both the prefixes and the @_xmlns keys.

Titles still contain &nbsp; after the conversion

CauseOnly the five entities defined by XML itself are expanded. Named entities such as &nbsp; or &mdash;, and anything a DTD would declare, stay as literal text because no DTD is ever loaded — <c>&nbsp;</c> comes out as "c": "&nbsp;". A bare & in text is likewise carried through without complaint.

FixDecode what is left after converting, in one pass rather than by hand: the HTML entity tool on this site, or python3 -c "import html,sys;print(html.unescape(sys.stdin.read()))" over the copied JSON.

The output pane vanishes and a red bar flashes while I am still typing

CauseThe result is a Vue computed that re-parses and re-stringifies the entire document on every keystroke, and it writes the error into reactive state as a side effect of that evaluation. A half-typed construct throws on that keystroke, so the banner replaces the output until a keystroke parses again.

FixPaste whole documents where you can; if you must edit in the page, finish each closing tag before moving to the next position. The banner clears by itself as soon as one parse succeeds — clearing the textarea is not required.

Chinese text pasted from an old GBK file shows up as question marks, yet the output says @_encoding GBK

CauseThe page only ever receives a JavaScript string, so the encoding declaration is carried into the JSON as the attribute @_encoding and never used to decode bytes. The damage happened before the text reached the textarea — in the editor that opened the file, or in the terminal that printed it.

FixConvert the file to UTF-8 first with iconv -f GBK -t UTF-8 config.xml and paste that, so the @_encoding left in the output is just a harmless record of what the file claims.

Six routes from XML to JSON, and when this paste box is not the one

What you are actually doingThis page (XML → JSON)What to reach for instead
The same conversion runs again tomorrow, against a fresh responseThe textarea is the only entry point: nothing is fetched, nothing is persisted, no previous document is remembered, so tomorrow means opening this tab and pasting again. Indentation has exactly two settings, 2 and 4, and there is no compact-output switch.One line in a script — curl -s https://api.example.com/orders.xml | yq -p xml -o json — makes the step repeatable and diffable and keeps the request in your history. Move there the second time you have to do it.
Amounts, invoice numbers and postal codes must land in the JSON unchangedAttribute values go through parseAttributeValue and text nodes through the default parseTagValue, so <price>12.00</price> arrives as 12 and a padded identifier such as 0042 as 42. No toolbar control disables the inference.python-xmltodict returns every leaf as a string, and fast-xml-parser with parseTagValue: false does the same. Choose one of those when the JSON feeds a ledger or a join key — the rewritten value cannot be recovered from this output.
One branch of a document with thousands of lines is all you needBoth panes always render the whole tree: no path box, no filter, no search. Whatever the widest node is, that is how far you scroll.Pull the slice with XPath — xmllint --xpath "//line[@qty>1]/@id" feed.xml or xmlstarlet sel -t -v "//line/@id" — then paste only that fragment here if you still want to read it indented.
The file has to become XML again later, comments and CDATA includedComments are discarded and CDATA is unwrapped into the text node, so XML → JSON → XML loses both constructs, and inline order inside mixed content is gone for good.Keep the XML as the source of truth and transform it with XSLT (xsltproc), or preserve the constructs with commentPropName and cdataPropName in a script. This page is for reading structure, not for a lossless hop.
The document is a SOAP envelope, or anything else with namespace prefixesPrefixes stay inside the keys: the root is literally "soap:Envelope", the payload sits under "soap:Body", and declarations survive as @_xmlns:soap, so data.Envelope is undefined and dot notation cannot reach any of it.Either index with brackets — data['soap:Envelope']['soap:Body'] — or run the same parser in a script with removeNSPrefix: true, which drops the prefixes and the @_xmlns entries in one pass.
You wanted XML at the end of this, not JSONThis page only goes one way. The JSON to XML tool on the site does understand the @_ and #text conventions this one emits, but it always wraps the result in an extra element (root unless you rename it) and pins the indent at 2 spaces.Use the JSON to XML tool when a throwaway copy is enough. For anything that must stay reviewable in a diff, write the XML from the original file as a template — an XSLT or a template keeps the declaration, the comments and attribute order.

Frequently Asked Questions

Where do XML attributes go in the JSON output?
This tool follows the widely used convention: attributes move into keys prefixed with @ ( becomes {"@id":"1"}), and text content goes under #text — the same convention as fast-xml-parser. Confirm which convention the target system expects, since libraries differ.
Why are all values strings after conversion?
XML has no number or boolean types — everything is text. Unless type inference is enabled (recognizing "123" as a number), values convert as strings. Add typing when wiring into strongly-typed APIs.
Why are keys so long with namespaced XML?
The tool keeps fully qualified names ( becomes "ns:item"), which is lossless. Stripping prefixes afterwards is fine for display, but merging blindly is risky: different prefixes may point to the same namespace.

Content reviewed: