XML→JSON Mapping Rules: Attributes, #text, and Array Handling
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
statusandtimestampbecome@statusand@timestamp - User attributes
idandverifiedbecome@idand@verified - Simple elements like
nameandemailbecome plain strings - Repeated
roleelements become an array - Each
rolehas an attribute and text content, so it becomes an object with@priorityand#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 #textbecomes 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 Pattern | JSON Result | Notes |
|---|---|---|
<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 null | Empty element |
<!-- comment --> | Omitted | No 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,#textfor 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 →
#textkey - 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.