#Working with JSON
One of the biggest misconceptions about XML technologies is that they can't work with JSON. In fact, XPath 3.1, XSLT 3.0, and XQuery 3.1 all have native JSON support — you can parse JSON, query it, transform it, and output it, all within the same tools you use for XML.
This means you can:
-
Parse a JSON API response and transform it into HTML
-
Combine JSON and XML data sources in a single query
-
Convert between JSON and XML formats
-
Use XSLT to produce JSON API responses from XML data
-
Query JSON with the full power of XPath
If you're a .NET developer who works primarily with JSON, this is the bridge between what you know and what XML tools offer.
#Contents
#Parsing JSON
#json-doc()
Loads and parses a JSON file into XDM maps and arrays:
json-doc('config.json')Given this JSON file:
{
"name": "PhoenixmlDb",
"version": "1.0.0",
"features": ["xquery", "xslt", "xpath"],
"settings": {
"debug": false,
"maxConnections": 100
}
}
json-doc('config.json') returns an XDM map — the same data structure you'd get from JsonSerializer.Deserialize<Dictionary<string, object>>() in C#, but queryable with XPath.
#parse-json()
Parses a JSON string (useful when JSON comes from an API call, parameter, or embedded in XML):
parse-json('{"name": "Alice", "age": 30}')
C# equivalent: JsonDocument.Parse(jsonString)
#What JSON Becomes in XDM
|
JSON |
XDM Type |
C# Equivalent |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Empty sequence |
|
#Querying JSON with XPath
Once JSON is parsed into maps and arrays, you query it with the ? lookup operator:
#Object Access
let $config := json-doc('config.json')
return (
$config?name, (: "PhoenixmlDb" :)
$config?version, (: "1.0.0" :)
$config?settings?debug, (: false :)
$config?settings?maxConnections (: 100 :)
)C# equivalent:
var config = JsonDocument.Parse(json);
config.RootElement.GetProperty("name").GetString();
config.RootElement.GetProperty("settings").GetProperty("debug").GetBoolean();The XPath version is more concise — $config?settings?debug vs. the verbose GetProperty chain.
#Array Access
let $config := json-doc('config.json')
return (
$config?features?1, (: "xquery" — 1-based indexing :)
$config?features?*, (: all features as a sequence :)
array:size($config?features) (: 3 :)
)#Filtering JSON Arrays
Given a JSON API response:
{
"users": [
{ "name": "Alice", "role": "admin", "active": true },
{ "name": "Bob", "role": "user", "active": true },
{ "name": "Charlie", "role": "user", "active": false }
]
}let $data := parse-json($json-string)
let $users := $data?users?* (: sequence of all user maps :)
(: Active users :)
return $users[.?active = true()]
(: Admin users :)
return $users[.?role = 'admin']
(: User names as a comma-separated string :)
return string-join($users ! .?name, ', ')
(: "Alice, Bob, Charlie" :)C# equivalent:
var users = JsonSerializer.Deserialize<List<User>>(json);
users.Where(u => u.Active).ToList();
users.Where(u => u.Role == "admin").ToList();
string.Join(", ", users.Select(u => u.Name));#Nested Queries
let $orders := json-doc('orders.json')?orders?*
for $order in $orders
where $order?total > 100
order by $order?total descending
return map {
'id': $order?id,
'customer': $order?customer?name,
'total': $order?total
}This is full FLWOR query power applied to JSON data — something that System.Text.Json alone can't do without writing procedural code.
#JSON in XSLT
#Reading JSON Input
An XSLT stylesheet can process JSON input by loading it with json-doc():
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0">
<xsl:output method="html" html-version="5" indent="yes"/>
<xsl:param name="data-file" select="'data.json'"/>
<xsl:variable name="data" select="json-doc($data-file)"/>
<xsl:template match="/">
<html>
<body>
<h1>Users</h1>
<table>
<tr><th>Name</th><th>Role</th></tr>
<xsl:for-each select="$data?users?*">
<tr>
<td><xsl:value-of select=".?name"/></td>
<td><xsl:value-of select=".?role"/></td>
</tr>
</xsl:for-each>
</table>
</body>
</html>
</xsl:template>
</xsl:stylesheet>This transforms a JSON file into an HTML table — using XSLT's template system and formatting capabilities, but reading from JSON instead of XML.
#Producing JSON Output
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0">
<xsl:output method="json" indent="yes"/>
<xsl:template match="catalog">
<xsl:sequence select="
map {
'products': array {
for $p in product
return map {
'id': string($p/@id),
'name': string($p/name),
'price': number($p/price),
'category': string($p/@category)
}
},
'count': count(product),
'generated': string(current-dateTime())
}
"/>
</xsl:template>
</xsl:stylesheet>Output:
{
"products": [
{ "id": "P001", "name": "Wireless Mouse", "price": 29.99, "category": "electronics" },
{ "id": "P002", "name": "Mechanical Keyboard", "price": 89.99, "category": "electronics" }
],
"count": 2,
"generated": "2026-03-19T14:30:00"
}#Combining JSON and XML Sources
<xsl:variable name="config" select="json-doc('config.json')"/>
<xsl:variable name="catalog" select="doc('catalog.xml')"/>
<xsl:template match="/">
<html>
<head><title><xsl:value-of select="$config?site?title"/></title></head>
<body>
<xsl:for-each select="$catalog//product">
<div class="product">
<h2><xsl:value-of select="name"/></h2>
<xsl:if test="$config?features?showPrices">
<p>$<xsl:value-of select="price"/></p>
</xsl:if>
</div>
</xsl:for-each>
</body>
</html>
</xsl:template>Configuration in JSON, data in XML, output in HTML — all in one stylesheet.
#JSON in XQuery
XQuery provides even more natural JSON handling:
#Querying JSON APIs
let $response := json-doc('https://api.example.com/products')
for $product in $response?data?*
where $product?price < 50
order by $product?name
return <li>{ $product?name } — ${ $product?price }</li>#Converting JSON to XML
let $users := json-doc('users.json')?*
return <users>{
for $user in $users
return <user id="{ $user?id }">
<name>{ $user?name }</name>
<email>{ $user?email }</email>
<roles>{
for $role in $user?roles?*
return <role>{ $role }</role>
}</roles>
</user>
}</users>#Converting XML to JSON
let $catalog := doc('catalog.xml')
return array {
for $p in $catalog//product
return map {
'id': string($p/@id),
'name': string($p/name),
'price': number($p/price),
'tags': array { $p/tags/tag ! string(.) }
}
}#Round-Trip: JSON to JSON
You can read JSON, process it with XPath/XQuery, and output JSON — never touching XML:
(: Read JSON, filter, reshape, output JSON :)
let $input := json-doc('orders.json')
let $orders := $input?orders?*
return map {
'summary': map {
'totalOrders': count($orders),
'totalRevenue': sum($orders ! .?total),
'averageOrder': avg($orders ! .?total)
},
'topCustomers': array {
let $by-customer := map:group-by($orders, function($o) { $o?customerId })
for $id in map:keys($by-customer)
let $customer-orders := $by-customer($id)
let $total := sum($customer-orders ! .?total)
order by $total descending
return map {
'customerId': $id,
'orderCount': count($customer-orders),
'totalSpent': $total
}
}
}Serialize with method="json" and you get a JSON-to-JSON transformation pipeline — no XML involved at all. The XQuery/XPath engine is acting as a general-purpose data transformation tool.
C# equivalent: This would require deserializing to objects, LINQ queries, then re-serializing. The XQuery version is more concise and declarative.
#Mixed Workflows: JSON + XML
Real-world data integration often combines formats. These tools handle that naturally:
#Enriching XML with JSON Config
let $products := doc('products.xml')//product
let $pricing := json-doc('pricing.json')
for $p in $products
let $price-info := $pricing?prices?*[.?sku = string($p/@sku)]
return <product id="{ $p/@id }">
{ $p/name }
<price currency="{ $price-info?currency }">{ $price-info?amount }</price>
<discount>{ $price-info?discount }</discount>
</product>#Generating JSON from XML + JSON Sources
let $catalog := doc('catalog.xml')
let $reviews := json-doc('reviews.json')
return array {
for $p in $catalog//product
let $product-reviews := $reviews?*[.?productId = string($p/@id)]
return map {
'product': string($p/name),
'price': number($p/price),
'avgRating': if (exists($product-reviews))
then avg($product-reviews ! .?rating)
else null,
'reviewCount': count($product-reviews)
}
}#JSON Output Patterns
#Pattern 1: Simple Object
map { 'status': 'ok', 'count': count(//item) }#Pattern 2: Array of Objects
array { //item ! map { 'name': string(./name), 'value': string(./price) } }#Pattern 3: Nested Structure
map {
'metadata': map { 'generated': string(current-dateTime()), 'version': '1.0' },
'data': array { ... }
}#Pattern 4: Preserving JSON Null
XPath's empty sequence () serializes as JSON null:
map {
'name': 'Alice',
'email': if ($user/email) then string($user/email) else ()
}
(: Output: { "name": "Alice", "email": null } :)#Serialization Control
serialize($map, map {
'method': 'json',
'indent': true(),
'allow-duplicate-names': false()
})#The Takeaway for .NET Developers
If your reaction is "wait, I can use XPath to query JSON?" — yes. The XPath 3.1 data model (maps + arrays) maps 1:1 to JSON's data model. This means:
-
You don't need to choose between JSON and XML tools. Use both.
-
XPath is a universal query language — it works on XML trees AND JSON data structures.
-
XSLT is a universal transform language — it can read JSON and produce JSON, not just XML-to-HTML.
-
Format conversion is natural — JSON ↔ XML ↔ HTML ↔ CSV ↔ text, all in one toolchain.
The "XML vs JSON" debate becomes moot when your tools handle both natively.