DEV Community

Cover image for How to Convert SOAP XML to JSON (and Back) Without Losing Attributes or Namespaces
Skojio Community
Skojio Community

Posted on • Originally published at skojio.com

How to Convert SOAP XML to JSON (and Back) Without Losing Attributes or Namespaces

Anyone who has had to debug a SOAP integration knows the moment: you're staring at a namespaced, attribute-heavy XML envelope, you need to inspect or mock it as JSON for a script or test fixture, and the free converter you paste it into either drops the attributes, strips the namespace prefixes, or turns a single <Role> element into a plain string one day and an array the next. None of that is a coincidence — it's what happens when a converter uses a fixed, undocumented convention instead of a stated one. This guide walks through what that convention actually needs to cover, using a SOAP envelope as the running example, since legacy SOAP and WSDL interop is one of the most common reasons anyone reaches for an XML-to-JSON tool in the first place.

Why SOAP and WSDL interop keeps needing this conversion

SOAP services, and the WSDL documents that describe them, are still very much alive in enterprise and government systems, banking middleware, and older internal APIs that nobody has had the time (or the budget) to migrate to REST. When a developer needs to inspect a SOAP response, write a test fixture against it, or mock its shape in a modern JavaScript or Python project, JSON is the natural working format — but hand-converting a real SOAP envelope quickly runs into exactly the details that trip up ad-hoc scripts: attributes, mixed text-and-element content, namespace prefixes, and elements that are sometimes singular and sometimes repeated.

A WSDL file is worth calling out specifically, because the honest answer for wsdl xml to json is more modest than it sounds: a WSDL document is just XML, so a general-purpose XML-to-JSON converter handles its structure fine — but it has no special understanding of what a <wsdl:operation> or <wsdl:binding> means semantically. That's a job for WSDL-aware tooling, not a format converter. Treating WSDL as generic, structurally-converted XML is the correct scope for this kind of tool, not a limitation to apologise for.

XML attributes to JSON: the @-prefix convention

Take a simple attributed element:

<GetUserResponse id="42">
  <Name>Ada Lovelace</Name>
</GetUserResponse>
Enter fullscreen mode Exit fullscreen mode

The most common lossless convention prefixes attribute names so they can never collide with a child element sharing the same name:

{
  "GetUserResponse": {
    "@id": "42",
    "Name": "Ada Lovelace"
  }
}
Enter fullscreen mode Exit fullscreen mode

Some converters instead offer a "merge attributes as plain keys" option, dropping the @ for a cleaner-looking {"id": "42", "Name": "..."}. That's a legitimate choice for simple documents, but it's genuinely lossy: if this element also had a child element literally named <id>, the attribute and the child would overwrite each other in the merged JSON with no warning. A converter should let you choose, but should also tell you, on the page, exactly when that choice is risky.

Element text, and when it needs a #text key

A plain leaf element with only text content should collapse to a simple JSON value — there's no reason <Name>Ada Lovelace</Name> needs to become {"Name": {"#text": "Ada Lovelace"}} when it has no attributes or children to also represent. The #text key (or whatever marker you configure) only earns its place once an element has both text content and attributes or children to sit alongside it:

<Price currency="GBP">42.50</Price>
Enter fullscreen mode Exit fullscreen mode
{
  "Price": { "@currency": "GBP", "#text": "42.50" }
}
Enter fullscreen mode Exit fullscreen mode

Getting this collapse rule right is what keeps the common case — a document full of simple leaf elements — from turning into noisy, wrapper-object JSON that's unpleasant to work with downstream.

Namespaces: keep the prefix, or strip it deliberately

This is where xml to json with namespaces queries usually run into trouble. A SOAP envelope is namespaced from the very first tag:

<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
  <soap:Body>
    <GetUserResponse id="42">
      <Name>Ada Lovelace</Name>
    </GetUserResponse>
  </soap:Body>
</soap:Envelope>
Enter fullscreen mode Exit fullscreen mode

The lossless default is to keep the prefix exactly as written in the JSON key — soap:Envelope, soap:Body — rather than resolving it to the namespace URI it's declared against, or silently stripping it to a bare Envelope. The xmlns:soap declaration itself just becomes an ordinary @xmlns:soap attribute on the element that declared it; it isn't specially resolved or merged across scopes. Stripping prefixes for a cleaner-looking key is a reasonable option — plenty of people genuinely don't need the prefix for their downstream use — but it should be a visible toggle, not the only behaviour on offer, because two differently-prefixed elements sharing a local name would otherwise collapse into the same JSON key and silently overwrite each other.


If you're converting a SOAP envelope specifically to inspect the body payload and don't care about the envelope/header wrapper, it's usually faster to convert the whole document once with namespaces preserved, then just navigate into soap:Body in the resulting JSON — rather than trying to strip the envelope out beforehand and risking a malformed fragment.

XML arrays to JSON: repeated elements, not a fixed schema

XML has no native array type — repetition is the only signal. When the same tag name appears more than once under the same parent, in document order, that's an array:

<User>
  <Name>Ada Lovelace</Name>
  <Role>Admin</Role>
  <Role>Analyst</Role>
</User>
Enter fullscreen mode Exit fullscreen mode
{
  "User": {
    "Name": "Ada Lovelace",
    "Role": ["Admin", "Analyst"]
  }
}
Enter fullscreen mode Exit fullscreen mode

The detail that trips up naive converters: a single occurrence of <Role> should stay a plain string, not get force-wrapped into a one-item array just because the schema theoretically allows repetition. If it did, every consumer of the JSON would have to write Array.isArray(user.Role) ? user.Role : [user.Role] defensively on every field that might repeat — which defeats the point of a clean conversion. Comments and processing instructions, meanwhile, are simply dropped; they were never part of the data, only the markup.

Going the other way: JSON to XML online, correctly

The reverse direction needs the literal inverse of every rule above, applied consistently:

{
  "GetUserResponse": {
    "@id": "42",
    "Name": "Ada Lovelace",
    "Role": ["Admin", "Analyst"]
  }
}
Enter fullscreen mode Exit fullscreen mode
<GetUserResponse id="42">
  <Name>Ada Lovelace</Name>
  <Role>Admin</Role>
  <Role>Analyst</Role>
</GetUserResponse>
Enter fullscreen mode Exit fullscreen mode

Keys matching your attribute prefix become attributes again, arrays repeat as sibling elements sharing the array's key as the tag name, and reserved XML characters (<, >, &, ", ') need correct escaping in both element text and attribute values. If the root JSON value is a single-key object, that key can become the XML root tag directly; an array or a multi-key object needs an explicit root element name, since XML — unlike JSON — always needs exactly one top-level element.

If you want to sanity-check that a JSON payload is well-formed before converting it to XML — particularly useful when you've hand-edited a fixture — a plain JSON formatter and validator is worth running first.

Round-trip fidelity: why the settings have to be shared, not duplicated

The real test of an XML ↔ JSON tool is whether XML → JSON → XML actually reproduces your original document. That only holds if the attribute prefix, text-content key, namespace handling, and array rule are the literal inverse of each other in both directions — which in practice means both directions need to read from one shared settings object, not two independently-built converters bolted together on the same page. A tool where "convert to JSON" and "convert to XML" were implemented separately, even by the same author, will drift the moment one side's default changes and the other doesn't.

Try it: an XML ↔ JSON converter built around round-trip fidelity

We built Skojio's XML ↔ JSON Converter specifically to make this convention visible, shared, and symmetric rather than fixed and hidden. It supports:

  • A configurable attribute prefix (@ or $, or merge-as-plain-keys with an on-page warning about when that's lossy)
  • A configurable text-content key, applied only when an element also has attributes or children
  • Namespace prefixes kept by default (the lossless choice), with an optional strip-prefix toggle
  • Repeated-sibling-to-array conversion that never force-wraps a single occurrence
  • A built-in "Load SOAP example" button that seeds a representative, namespaced envelope so you can see the whole conversion in one click
  • A one-click "Verify round-trip" action that sends your output straight back through the other direction

Everything runs in your browser via the native DOMParser and hand-rolled JavaScript — nothing you paste or upload is sent to a server, and there's no external entity or DTD fetching, so there's no XXE risk either. It's a natural complement to JSON Formatter & Validator for checking structure first, and to JSON ↔ CSV Converter if the same data eventually needs to land in a spreadsheet instead of a legacy XML endpoint.

  • SOAP and WSDL interop is one of the most common reasons developers need an XML ↔ JSON converter — treat WSDL as generic XML, not something requiring semantic understanding of its operations.
  • XML attributes need a visible, consistent convention (commonly an @ prefix) to avoid silently colliding with child elements of the same name.
  • Namespace prefixes should be kept by default (soap:Envelope), not silently stripped or resolved — stripping is a lossy option, not the only behaviour.
  • Repeated sibling elements become a JSON array in document order; a single occurrence must stay a plain value, never force-wrapped.
  • Round-trip fidelity (XML → JSON → XML) only holds if both directions share one settings object rather than independently guessing at the same conventions.

Top comments (0)