XSLT Playground logo XSLT Playground
Powered by Saxon HE 12.5

XML to JSON Online

Convert XML to JSON with XSLT 3.0 and decide the exact shape of the result — keys, nesting, arrays and types. Not a guess: a transformation you control.

Open the XML → JSON Converter →
There is no canonical mapping from XML to JSON. XML has attributes, element order, mixed content, namespaces and repeated siblings. JSON has objects, arrays, strings, numbers, booleans and null. Any generic converter must invent a convention to bridge the two — and its convention is rarely the one your API consumer expects. XSLT 3.0 lets you write the mapping explicitly instead.

Why Convert XML to JSON with XSLT 3.0?

Generic online converters make three decisions on your behalf, silently, and all three are frequently wrong:

With XSLT 3.0 you declare each of those choices. An array stays an array whether it holds one item or a thousand, a numeric field is a number because you said so, and keys are named to match the contract you are publishing rather than the element names you happened to receive. You also get everything else XSLT offers along the way — grouping, sorting, filtering, aggregation and lookups into secondary documents — in the same pass as the conversion.

The Two Ways to Produce JSON in XSLT 3.0

Maps and arrays + method="json"

Build a native XPath 3.1 map{} or array{} and let the serialiser write it out with xsl:output method="json". The most concise route for straightforward structures.

Node vocabulary + xml-to-json()

Build elements in the XPath functions namespace and pass them to xml-to-json(). Because you are producing nodes, you can use templates, xsl:for-each-group and xsl:sort to assemble the structure.

The JSON-XML Vocabulary

Both xml-to-json() and json-to-xml() speak a small XML vocabulary in the namespace http://www.w3.org/2005/xpath-functions. Learning these six element names is most of what the conversion requires:

ElementJSON produced
<map>An object { … }. Each child carries a key attribute.
<array>An array [ … ]. Children have no key unless the array is itself a map member.
<string>A quoted string; escaping is handled for you.
<number>An unquoted JSON number.
<boolean>true or false.
<null/>null.

A member of a map needs key="name"; an item of an array does not. Getting a key wrong is the single most common cause of an FOJS0006 error from xml-to-json().

Example 1: Maps and Arrays with method="json"

The shortest path from XML to JSON. Build a map, let the serialiser do the rest. Every example on this page runs on Saxon HE 12.5 — paste it into the editor to reproduce the output exactly.

XML input:

<orders>
  <order id="1" country="ES" total="120.50"/>
  <order id="2" country="FR" total="80.00"/>
  <order id="3" country="ES" total="45.25"/>
  <order id="4" country="DE" total="200.00"/>
</orders>

Stylesheet:

<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="json" indent="yes"/>

  <xsl:template match="/">
    <xsl:sequence select="map{
      'orderCount': count(orders/order),
      'countries': array{ distinct-values(orders/order/@country) },
      'grandTotal': sum(orders/order/@total)
    }"/>
  </xsl:template>
</xsl:stylesheet>

Output:

{
  "orderCount": 4,
  "countries": [ "ES", "FR", "DE" ],
  "grandTotal": 445.75
}

Note array{ … } with curly braces: it builds one array from the whole sequence returned by the expression. The square-bracket form array[ … ] would instead create one member per argument.

Example 2: The Node Vocabulary with xml-to-json()

This is the stylesheet behind the XML → JSON template. It converts each order element into a JSON object inside an array, choosing per field whether the value is a number or a string. The default namespace on <array> puts the whole structure into the XPath functions namespace, so every descendant inherits it.

Stylesheet:

<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text"/>

  <xsl:template match="/">
    <xsl:variable name="data">
      <array xmlns="http://www.w3.org/2005/xpath-functions">
        <xsl:for-each select="orders/order">
          <map>
            <number key="id"><xsl:value-of select="@id"/></number>
            <string key="country"><xsl:value-of select="@country"/></string>
            <number key="total"><xsl:value-of select="@total"/></number>
          </map>
        </xsl:for-each>
      </array>
    </xsl:variable>
    <xsl:value-of select="xml-to-json($data, map{'indent': true()})"/>
  </xsl:template>
</xsl:stylesheet>

Output:

 [
  { "id" : 1,
   "country" : "ES",
   "total" : 120.5 },

  { "id" : 2,
   "country" : "FR",
   "total" : 80 },

  { "id" : 3,
   "country" : "ES",
   "total" : 45.25 },

  { "id" : 4,
   "country" : "DE",
   "total" : 200 } ]

Two details worth noticing. First, the indent option is advisory — the JSON is correct but the exact whitespace is processor-defined, so never diff serialised JSON against a golden file character by character. Second, 120.50 came out as 120.5 and 200.00 as 200, because JSON numbers have no trailing zeros.

Need to keep the trailing zeros? Emit the field as <string key="total"> instead of <number>, or format it first with format-number(@total, '0.00'). This is exactly the kind of decision a generic converter takes away from you.

Example 3: Reshaping — Grouping XML into Nested JSON

Here is what full control actually buys you. Instead of mirroring the input structure, this stylesheet groups the orders by country, aggregates a total per group, sorts the groups, and emits a nested document with a metadata header. No generic converter can produce this, because it is not a mapping of the input — it is a transformation of it.

Stylesheet:

<xsl:stylesheet version="3.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:fn="http://www.w3.org/2005/xpath-functions"
  exclude-result-prefixes="fn">
  <xsl:output method="text"/>

  <xsl:template match="/">
    <xsl:variable name="json-xml">
      <fn:map>
        <fn:string key="generated">2026-07-22</fn:string>
        <fn:number key="orderCount"><xsl:value-of select="count(orders/order)"/></fn:number>
        <fn:array key="countries">
          <xsl:for-each-group select="orders/order" group-by="@country">
            <xsl:sort select="current-grouping-key()"/>
            <fn:map>
              <fn:string key="code"><xsl:value-of select="current-grouping-key()"/></fn:string>
              <fn:number key="total"><xsl:value-of select="sum(current-group()/@total)"/></fn:number>
              <fn:array key="orderIds">
                <xsl:for-each select="current-group()">
                  <fn:number><xsl:value-of select="@id"/></fn:number>
                </xsl:for-each>
              </fn:array>
            </fn:map>
          </xsl:for-each-group>
        </fn:array>
      </fn:map>
    </xsl:variable>
    <xsl:value-of select="xml-to-json($json-xml, map{'indent': true()})"/>
  </xsl:template>
</xsl:stylesheet>

Output:

 { "generated" : "2026-07-22",
  "orderCount" : 4,
  "countries" :
  [
   { "code" : "DE",
    "total" : 200,
    "orderIds" :
    [ 4 ] },

   { "code" : "ES",
    "total" : 165.75,
    "orderIds" :
    [ 1,
     3 ] },

   { "code" : "FR",
    "total" : 80,
    "orderIds" :
    [ 2 ] } ] }

Note that orderIds stays a JSON array even for DE, which has a single order. That consistency is the whole point — a consumer can always index it without checking the type first.

Example 4: The Other Direction — json-to-xml()

The same stylesheet can go the other way. json-to-xml() parses a JSON string into the same vocabulary, which you then navigate with ordinary XPath using the fn: prefix. This example needs no source document at all — it runs from the XSLT 3.0 named entry point.

Stylesheet:

<xsl:stylesheet version="3.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  xmlns:fn="http://www.w3.org/2005/xpath-functions"
  exclude-result-prefixes="xs fn">
  <xsl:output method="xml" indent="yes"/>

  <xsl:variable name="json" as="xs:string">{"users":[{"name":"Ada","active":true},{"name":"Linus","active":false}]}</xsl:variable>

  <xsl:template name="xsl:initial-template">
    <users>
      <xsl:for-each select="json-to-xml($json)/fn:map/fn:array[@key='users']/fn:map">
        <user name="{fn:string[@key='name']}" active="{fn:boolean[@key='active']}"/>
      </xsl:for-each>
    </users>
  </xsl:template>
</xsl:stylesheet>

Output:

<users>
   <user name="Ada" active="true"/>
   <user name="Linus" active="false"/>
</users>

If you would rather work with native maps than nodes, parse-json() returns an XPath 3.1 map you can navigate with the lookup operator — parse-json($json)?users?*?name yields Ada Linus. Use json-doc() instead when the JSON lives at a URI rather than in a string.

Frequently Asked Questions

How do I convert XML to JSON with XSLT 3.0?
Two ways: build an XPath 3.1 map{} or array{} and serialise it with xsl:output method="json", or build nodes in the XPath functions namespace and pass them to xml-to-json(). Both are shown above and both run on Saxon HE 12.5.
Why use XSLT instead of a generic XML to JSON converter?
Because there is no canonical XML-to-JSON mapping, a generic converter has to guess how attributes are named, whether a single repeated element is an array, and which strings are really numbers. XSLT 3.0 lets you state each decision explicitly, so the JSON matches the contract your consumer expects.
What is the difference between xml-to-json() and json-to-xml()?
xml-to-json() turns nodes in the XPath functions namespace into a JSON string. json-to-xml() parses a JSON string into that same vocabulary so you can process it with ordinary XPath and templates.
Why did 120.50 become 120.5?
JSON numbers have no trailing zeros, so a <number> is serialised in canonical form. Emit the value as a <string>, or pre-format it with format-number(), if the exact digits matter.
Can I convert JSON back to XML online?
Yes. Use json-to-xml() for a node tree, or parse-json() for a native map you can navigate with the ? lookup operator. Select XSLT 3.0 and both are available.
Is the online XML to JSON converter free?
Yes — free, no installation, no account. XSLT Playground is the only free online tool with full XSLT 3.0 support, which is what makes this level of control possible in a browser.

Function Reference

Every XSLT and XPath function, documented with syntax and a runnable example: browse the complete XSLT & XPath function reference. The entries that matter most for JSON work:

Learn More

Also Available

Still working out which nodes to select before you convert them? Try the XPath Tester →

Everything here needs XSLT 3.0 — see the XSLT 3.0 Online Tester. For grouping and regular expressions without the JSON layer, see the XSLT 2.0 Online Tester.