n8n XML node: converting XML to JSON (and back) without surprises
Published 5 August 2026 · 6 min read
JSON won, but XML never signed its surrender: banking or logistics SOAP APIs, e-commerce product feeds, sitemaps, EDI invoices, government web services — in 2026, an n8n integrator still runs into XML every week. The good news: n8n's XML node converts both ways, XML → JSON for parsing, JSON → XML to answer a legacy system. The less good news: its default options produce JSON whose shape changes depending on the file's contents, and that is the number one cause of workflows that work in testing and break in production. This guide covers the node, its options, and a complete product feed → Google Sheets pipeline.
Why you still run into XML in 2026
XML survives wherever interface contracts were written fifteen years ago and nobody wants to pay for the migration: SOAP web services (banks, insurers, carriers), marketplace and price-comparison product feeds, sitemaps and RSS feeds (an RSS feed is XML — the RSS node parses it for you, but an exotic feed gets handled manually), EDI exchanges and public-sector e-procedures.
The format lost the battle for the web for measurable reasons: the study by Nurzhan Nurseitov, Michael Paulson, Randall Reynolds, and Clemente Izurieta, "Comparison of JSON and XML Data Interchange Formats: A Case Study" (CAINE, 2009), showed across thousands of transmitted objects that JSON was faster to transmit and less resource-hungry than XML (see on Google Scholar). More recently, J. Gerrans and R. S. Sherratt (IEEE Embedded Systems Letters, 2024) measured a JSON file 24.7% smaller than its XML equivalent for identical data (see on Google Scholar). But a supplier feed is not something you get to renegotiate: you convert.
"XML to JSON" mode: the data must be a string
The XML node reads neither URLs nor files: it expects the XML to already be present as a string in a field of the item. Its configuration comes down to two parameters:
- Mode:
XML to JSON; - Property Name: the name of the field containing the XML string (default
data). The JSON result replaces the string in that same field.
In practice, the XML almost always arrives through an HTTP Request node: in its options, add Response → Response Format: Text, so the response body lands as-is in data instead of being interpreted. Chain the XML node with data as Property Name, and it's parsed. If the feed is paginated (some legacy APIs are), the techniques from our API pagination guide apply unchanged — only the body format differs.
The options that change everything
Explicit Array: the classic trap
This is the node's most important option. When it's off, the converter simplifies: an element that appears once becomes an object, one that appears several times becomes an array. The same product feed therefore yields two different structures depending on its contents:
// only 1 <product> in the feed: object
{ "catalog": { "product": { "name": "Organic cotton t-shirt" } } }
// 2 <product> or more: array
{ "catalog": { "product": [ { "name": "Organic cotton t-shirt" }, { "name": "Enamel mug" } ] } }
Your workflow tested on a 500-product feed works fine, then breaks on the slow day when the feed only contains one. Turn Explicit Array on systematically: every child element becomes an array, always, and parsing is deterministic. The price to pay — slightly more verbose expressions like {{ $json.catalog.product[0].name[0] }} — is more than offset by the stability.
Attributes: Attribute Key, Ignore Attributes, Merge Attributes
XML carries data in attributes (<product sku="TSH-001">), a concept with no JSON equivalent. Three options decide their fate:
- Attribute Key: the prefix under which attributes are grouped —
$by default, so{{ $json.product['$'].sku }}; - Ignore Attributes: drops them entirely — only for feeds where they carry nothing useful;
- Merge Attributes: merges attributes with child elements at the same level —
skubecomes an ordinary field next toname, which yields the most natural JSON to work with. It's often the best choice, unless an attribute name collides with a child tag.
Cleanup: Trim, Normalize, Explicit Root
Trim removes leading and trailing whitespace from text nodes, Normalize normalizes internal whitespace — two options worth enabling on hand-indented feeds where <price> 24.90 </price> would break a numeric conversion. Explicit Root controls whether the root element appears in the result; turned off, you save one level (catalog) in every expression.
Complete example: XML product feed → Google Sheets
The supplier's feed:
<catalog>
<product sku="TSH-001">
<name>Organic cotton t-shirt</name>
<price>24.90</price>
<stock>142</stock>
</product>
<product sku="MUG-014">
<name>Enamel mug</name>
<price>14.50</price>
<stock>0</stock>
</product>
</catalog>
After HTTP Request (Response Format: Text) then XML (Explicit Array and Merge Attributes enabled):
{
"catalog": {
"product": [
{ "sku": "TSH-001", "name": ["Organic cotton t-shirt"], "price": ["24.90"], "stock": ["142"] },
{ "sku": "MUG-014", "name": ["Enamel mug"], "price": ["14.50"], "stock": ["0"] }
]
}
}
Everything is still inside a single n8n item. A Split Out node on the catalog.product field turns the array into one item per product — the pivot of any list processing, detailed in our Split Out and Aggregate guide. Then: a Filter discards out-of-stock items ({{ Number($json.stock[0]) > 0 }}), an Edit Fields node flattens and converts types ({{ Number($json.price[0]) }}), and a Google Sheets node in "Append or Update" mode with sku as the key keeps the sheet in sync — the full Sheets setup is in our Google Sheets guide.
Namespaces and SOAP: parsing an envelope
SOAP responses arrive prefixed:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetStockResponse><quantity>142</quantity></GetStockResponse>
</soap:Body>
</soap:Envelope>
The XML node converts them without complaint, but the colons in the keys (soap:Envelope) rule out dot notation. Use bracket notation: {{ $json['soap:Envelope']['soap:Body'][0]['GetStockResponse'][0].quantity[0] }}. The node exposes no option to strip prefixes; if the chain of brackets becomes unreadable, a three-line Code node can rename the keys after conversion. For the call itself: an HTTP Request in POST, the XML envelope in the body, Content-Type: text/xml and the SOAPAction header the service requires.
The reverse mode: JSON to XML
The same node generates XML from your items: Mode JSON to XML, and the options change — Root Name (the name of the root element), Headless (whether to omit the <?xml ... ?> prolog), Cdata (wrap text containing special characters in <![CDATA[ ... ]]> instead of escaping it). It's the building block for posting to a legacy API that only accepts XML, generating a sitemap, or producing a feed for a partner — the XML counterpart of generating CSV or Excel files.
The remaining pitfalls
- Encoding: a feed declared
ISO-8859-1in its prolog can come out with broken accented characters. Check the HTTP response's actual charset and, if needed, pass the response through as binary and extract the text while specifying the encoding. - CDATA: when reading, the content of
<![CDATA[ ... ]]>sections comes out as normal text — nothing to do. It's when generating that you need to think of the Cdata option if your values contain<,&, or HTML. - Large files: the XML node loads the entire string into memory. A catalog of several tens of MB should be fetched as binary data and processed in chunks — the right reflexes are detailed in our guide to large files in n8n.
Summary
n8n's XML node makes both trips: XML to JSON for parsing (the data must be a string in the field named by Property Name, typically via HTTP Request with Response Format Text), JSON to XML for generating. Turn on Explicit Array from the very first test — it's the node's most profitable insurance against breakage — and decide the fate of attributes with Merge Attributes or Attribute Key. Chained with Split Out, a filter, and Google Sheets, it turns any legacy feed into a clean pipeline. And if your goal is precisely to make heterogeneous sources — XML feeds, documents, exports — usable by an AI that answers your team's questions, the RAG Assistant Pack provides the ingestion and querying chain ready to plug in behind this kind of conversion.
FAQ
Frequently asked questions
Why does the n8n XML node sometimes return an object and sometimes an array for the same element?
That's the Explicit Array option at work. When it's off, an element that appears once comes out as an object, while the same element appearing several times comes out as an array: your workflow breaks the day the feed only contains one record. Turn Explicit Array on so every child element is always an array, then write your expressions accordingly — parsing becomes deterministic.
How do I fetch XML with the HTTP Request node before converting it?
In the HTTP Request node's options, add the Response option and set Response Format to Text: the XML body then lands as a plain string in the item's data field. That is exactly what the XML node expects: set data as the Property Name in XML to JSON mode. Without this setting, n8n may try to interpret the response and the XML node won't receive a valid string.
Can you call a SOAP API with n8n even though there is no dedicated SOAP node?
Yes: a SOAP request is just an HTTP POST with an XML body (the envelope), usually a SOAPAction header and a text/xml Content-Type. Send the envelope with HTTP Request, fetch the response as Text, then convert it with the XML node. Prefixed tags like soap:Body are then reached with bracket notation in expressions, for example $json['soap:Envelope']['soap:Body'].
Bundle FlowKit Complet
€269