DevToys Pro

free web developer tools

Blog
Rate us:
Try browser extension:
← Back to Blog

XML→JSON Mapping Rules: Attributes, #text, and Array Handling

16 min read

You convert XML to JSON and the output doesn't match your expectations: attributes become @attr, text content becomes #text, and sometimes arrays appear where you expected objects. Understanding XML↔JSON mapping rules helps you predict conversion output and structure your data correctly for round-trip conversions.

The Fundamental Problem: XML ≠ JSON

XML and JSON have different data models:

  • XML: Elements can have attributes, text content, and child elements simultaneously
  • JSON: Objects have key-value pairs; no distinction between attributes and children

There's no single "correct" way to convert between them, but common conventions have emerged.

Rule 1: Elements Become Objects

Simple XML elements convert to JSON objects:

<!-- XML -->
<person>
  <name>Alice</name>
  <age>30</age>
</person>
{
  "person": {
    "name": "Alice",
    "age": "30"
  }
}

Rule 2: Attributes Get Special Prefix

XML attributes typically become JSON keys with an @ prefix:

<!-- XML -->
<person id="123" active="true">
  <name>Alice</name>
</person>
{
  "person": {
    "@id": "123",
    "@active": "true",
    "name": "Alice"
  }
}

The @ prefix distinguishes attributes from child elements. Without it, you couldn't tell <person id="123"> from <person><id>123</id></person>.

Rule 3: Text Content Becomes #text

When an element has both attributes and text content, the text becomes a special #text key:

<!-- XML -->
<price currency="USD">49.99</price>
{
  "price": {
    "@currency": "USD",
    "#text": "49.99"
  }
}

Without the #text key, there'd be no way to represent both the attribute currency and the text value 49.99.

Simple Text (No Attributes) = Direct Value

If an element has only text content (no attributes, no children), it becomes a simple string:

<!-- XML -->
<name>Alice</name>
{
  "name": "Alice"
}

No #text wrapper needed because there's no ambiguity.

Rule 4: Repeated Elements Become Arrays

Multiple elements with the same name convert to a JSON array:

<!-- XML -->
<items>
  <item>Widget A</item>
  <item>Widget B</item>
  <item>Widget C</item>
</items>
{
  "items": {
    "item": [
      "Widget A",
      "Widget B",
      "Widget C"
    ]
  }
}

Single Element ≠ Array

This is a common pitfall. A single element doesn't become an array:

<!-- XML -->
<items>
  <item>Widget A</item>
</items>
{
  "items": {
    "item": "Widget A"    // String, not array!
  }
}

This means your code must check whether item is a string or an array. Some converters have an "always use arrays" option to avoid this inconsistency.

Rule 5: Empty Elements Become Empty Objects or Null

Empty XML elements typically become empty objects or null:

<!-- XML -->
<data>
  <empty></empty>
  <selfClosing/>
</data>
{
  "data": {
    "empty": {},
    "selfClosing": {}
  }
}

Or, depending on the converter:

{
  "data": {
    "empty": null,
    "selfClosing": null
  }
}

Real-World Example: API Response

Here's a realistic SOAP API response and its JSON conversion:

XML API Response

<?xml version="1.0" encoding="UTF-8"?>
<response status="success" timestamp="2026-01-17T10:30:00Z">
  <user id="12345" verified="true">
    <name>Alice Johnson</name>
    <email>alice@example.com</email>
    <roles>
      <role priority="high">admin</role>
      <role priority="normal">user</role>
    </roles>
    <metadata>
      <created>2020-01-15</created>
      <lastLogin>2026-01-16T08:00:00Z</lastLogin>
    </metadata>
  </user>
</response>

JSON Conversion

{
  "response": {
    "@status": "success",
    "@timestamp": "2026-01-17T10:30:00Z",
    "user": {
      "@id": "12345",
      "@verified": "true",
      "name": "Alice Johnson",
      "email": "alice@example.com",
      "roles": {
        "role": [
          {
            "@priority": "high",
            "#text": "admin"
          },
          {
            "@priority": "normal",
            "#text": "user"
          }
        ]
      },
      "metadata": {
        "created": "2020-01-15",
        "lastLogin": "2026-01-16T08:00:00Z"
      }
    }
  }
}

Key observations:

  • Root attributes status and timestamp become @status and @timestamp
  • User attributes id and verified become @id and @verified
  • Simple elements like name and email become plain strings
  • Repeated role elements become an array
  • Each role has an attribute and text content, so it becomes an object with @priority and #text

Converting JSON Back to XML

The reverse conversion (JSON → XML) follows the same rules but requires careful handling:

{
  "order": {
    "@id": "ORD-001",
    "customer": "Alice",
    "items": {
      "item": [
        "Widget A",
        "Widget B"
      ]
    },
    "total": {
      "@currency": "USD",
      "#text": "99.99"
    }
  }
}

Converts to:

<order id="ORD-001">
  <customer>Alice</customer>
  <items>
    <item>Widget A</item>
    <item>Widget B</item>
  </items>
  <total currency="USD">99.99</total>
</order>

Rules applied:

  • Keys starting with @ become attributes
  • #text becomes text content
  • Arrays become repeated elements
  • Simple string values become text content

Namespaces: The Complication

XML namespaces add complexity to JSON conversion:

<!-- XML with namespace -->
<order xmlns="http://example.com/orders" xmlns:cust="http://example.com/customer">
  <cust:customer id="123">
    <cust:name>Alice</cust:name>
  </cust:customer>
  <item>Widget</item>
</order>

Different converters handle this differently:

Option 1: Preserve Prefixes

{
  "order": {
    "@xmlns": "http://example.com/orders",
    "@xmlns:cust": "http://example.com/customer",
    "cust:customer": {
      "@id": "123",
      "cust:name": "Alice"
    },
    "item": "Widget"
  }
}

Option 2: Expand Namespaces

{
  "{http://example.com/orders}order": {
    "{http://example.com/customer}customer": {
      "@id": "123",
      "{http://example.com/customer}name": "Alice"
    },
    "{http://example.com/orders}item": "Widget"
  }
}

Use XML ↔ JSON Converter to test how your namespaced XML converts.

Mixed Content: The Edge Case

Mixed content (text and elements mixed together) is valid XML but awkward in JSON:

<!-- XML with mixed content -->
<description>
  The price is <price currency="USD">49.99</price> for new customers only.
</description>

This can't be cleanly represented in JSON because JSON objects don't preserve order or allow text between keys. Converters typically handle this by creating an array:

{
  "description": [
    "The price is ",
    {
      "price": {
        "@currency": "USD",
        "#text": "49.99"
      }
    },
    " for new customers only."
  ]
}

Mixed content is rare in data-oriented XML (like APIs) but common in document-oriented XML (like HTML or DocBook).

CDATA Sections

CDATA sections preserve special characters without escaping:

<!-- XML -->
<code><![CDATA[
  if (x < 10 && y > 5) {
    console.log("Hello");
  }
]]></code>

In JSON, CDATA becomes plain text:

{
  "code": "\n  if (x < 10 && y > 5) {\n    console.log(\"Hello\");\n  }\n"
}

The CDATA wrapper is lost, but the content is preserved with proper escaping.

Comments Are Lost

XML comments don't convert to JSON (JSON has no comment syntax):

<!-- XML -->
<config>
  <!-- This is important -->
  <setting>value</setting>
</config>
{
  "config": {
    "setting": "value"
  }
}

Comments are silently discarded during conversion.

Real-World Debugging Scenario

You receive XML from a SOAP API and need to work with it in JavaScript. Here's the workflow:

1. Convert XML to JSON

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <GetUserResponse xmlns="http://api.example.com">
      <user id="12345">
        <name>Alice</name>
        <roles>
          <role>admin</role>
          <role>user</role>
        </roles>
      </user>
    </GetUserResponse>
  </soap:Body>
</soap:Envelope>

Paste into XML ↔ JSON Converter

2. Understand the JSON Structure

{
  "soap:Envelope": {
    "@xmlns:soap": "http://schemas.xmlsoap.org/soap/envelope/",
    "soap:Body": {
      "GetUserResponse": {
        "@xmlns": "http://api.example.com",
        "user": {
          "@id": "12345",
          "name": "Alice",
          "roles": {
            "role": ["admin", "user"]
          }
        }
      }
    }
  }
}

3. Access Data in JavaScript

const envelope = JSON.parse(jsonString);
const user = envelope["soap:Envelope"]["soap:Body"].GetUserResponse.user;

console.log(user["@id"]);      // "12345"
console.log(user.name);         // "Alice"
console.log(user.roles.role);   // ["admin", "user"]

// Check if role is array (handles single vs multiple roles)
const roles = Array.isArray(user.roles.role) 
  ? user.roles.role 
  : [user.roles.role];

roles.forEach(role => console.log(role));

Common Pitfalls and Solutions

Pitfall 1: Single vs Multiple Elements

Problem: Code breaks when XML has one <item> instead of multiple

Solution: Always check if value is an array

// Defensive parsing
const items = data.items.item;
const itemArray = Array.isArray(items) ? items : [items];

itemArray.forEach(item => {
  console.log(item);
});

Pitfall 2: Accessing Attributes

Problem: Forgetting the @ prefix for attributes

Solution: Remember attributes have @ prefix

// Wrong
console.log(user.id);        // undefined

// Correct
console.log(user["@id"]);    // "12345"
// Or use bracket notation
console.log(user["@id"]);

Pitfall 3: Text + Attributes

Problem: Expecting simple value when element has attributes

<price currency="USD">49.99</price>
// Wrong - price is an object, not a string
console.log(data.price);  // { "@currency": "USD", "#text": "49.99" }

// Correct
console.log(data.price["#text"]);      // "49.99"
console.log(data.price["@currency"]); // "USD"

Tools for Testing Conversions

Use XML ↔ JSON Converter to:

  • Test XML → JSON conversion with real data
  • See how attributes, arrays, and namespaces are handled
  • Verify round-trip conversions (XML → JSON → XML)
  • Debug API response structures

Quick Reference: Mapping Rules

XML PatternJSON ResultNotes
<name>Alice</name>{"name": "Alice"}Simple element → string
<user id="123">{"@id": "123"}Attribute → @key
<price currency="USD">49.99</price>{"@currency": "USD", "#text": "49.99"}Attribute + text
<item>A</item><item>B</item>{"item": ["A", "B"]}Repeated → array
<item>A</item> (single){"item": "A"}Single → not array
<empty/>{"empty": {}} or nullEmpty element
<!-- comment -->OmittedNo JSON equivalent
<![CDATA[...]]>Plain text (escaped)CDATA wrapper lost
<ns:element>{"ns:element": ...}Namespace prefix preserved

Best Practices

  • Test conversions - Use converter tools with real data before writing code
  • Handle arrays defensively - Always check if value is array vs single item
  • Remember prefixes - @ for attributes, #text for mixed content
  • Document structure - Note which fields are arrays, which are attributes
  • Avoid mixed content - Design data-oriented XML without text between elements
  • Consider schemas - XSD schemas help predict array vs single element

Summary

XML to JSON conversion follows predictable rules:

  • Elements → objects, attributes → @key
  • Text with attributes → #text key
  • Repeated elements → arrays (but single element stays singular)
  • Empty elements → empty objects or null
  • Namespaces preserved as prefixes or expanded URIs
  • Comments and CDATA sections lost/flattened

Understanding these rules helps you predict conversion output and write robust parsing code. Use XML ↔ JSON Converter to test conversions with your actual data.


Need to convert between XML and JSON? Use XML ↔ JSON Converter to test conversions and understand how your data will be structured. Also check out JSON Formatter for cleaning up converted JSON.