Reading and writing JSON¶
XSLT grew up transforming XML into XML or HTML, but the data you are handed today
is just as likely to be JSON — a REST payload, a config file, a log line.
XSLT 3.0 makes JSON a first-class citizen: you can parse it into native maps
and arrays, walk it with a compact lookup syntax, and serialise structured
data straight back out as JSON. None of this exists in 1.0 or 2.0 — everything on
this page needs version="3.0" and a 3.0 processor such as Saxon.
There are two distinct ways to handle JSON, and it is worth knowing both:
| Approach | You get | Reach for it when |
|---|---|---|
Maps & arrays (parse-json, json-doc) |
Native XDM maps/arrays, navigated with ? |
You want to consume JSON as data |
XML representation (json-to-xml, xml-to-json) |
An XML tree of <map>/<array>/<string> elements |
You want to reuse your XPath/template skills on JSON |
Reading JSON into maps and arrays¶
parse-json($string) turns a JSON string into native values: a JSON object
becomes a map, a JSON array becomes an array, and the scalars become
xs:string, xs:double, xs:boolean, or the empty sequence for null.
| parse.xsl | |
|---|---|
- A literal JSON string. In real life this would come from a parameter, a file, or a web response.
parse-jsonreturns amap(xs:string, item()*)here — keys are strings, values are whatever the JSON held.- The
?lookup operator reads a map entry by key:$cd?artistis "look up keyartistin map$cd". Far terser thanmap:get($cd, 'artist').
Bob Dylan
json-doc() reads straight from a URI
When the JSON lives in a file or at a URL, skip the read-then-parse two-step:
json-doc('catalog.json') fetches and parses in one call, returning the
same maps and arrays as parse-json. It is the JSON counterpart of
document().
The lookup operator ?¶
? is the workhorse for navigating parsed JSON. It indexes maps by key and
arrays by position (arrays are 1-based, like everything else in XPath), and it
chains:
- Chain three lookups: key
catalog→ array index1→ keytitle. →Empire Burlesque. - The wildcard
?*selects every member of the array, so?*?titleis "the title of each entry" — a sequence of two strings. - Because
?*?priceis a real sequence, ordinary XPath functions likesumapply directly. →20.8.
When the key is not a name
$map?artist only works when the key is a valid name token. For keys with
spaces or computed keys, use the parenthesised form $map?('unit price') or
fall back to map:get($map, 'unit price').
Iterating over parsed JSON¶
Arrays are not sequences, so you do not for-each over them directly — you turn
them into a sequence first. ?* is the usual way; array:members and the arrow
operator also work. Each member is itself a map you can look into:
| iterate.xsl | |
|---|---|
?*expands the array into a sequence of member maps, one perfor-eachiteration.- Inside the loop the context item is one map, so a bare
?titlelooks up the current entry. The{ }here is a text value template (see Modern identity and text); enable it withexpand-text="yes"on the stylesheet.
Building JSON: maps and arrays¶
To emit JSON you build the native structures and then serialise them. Construct
maps and arrays inline with the map { } and array { } constructors, or
element-by-element with xsl:map / xsl:map-entry:
| build.xsl | |
|---|---|
- A map literal:
key : valuepairs separated by commas. Keys here are strings; values are an integer, an array, and a decimal. array { ... }collects a sequence into a JSON array — eachtitle's string value becomes one element.
The xsl:map form is handy when entries are conditional or built in a loop:
| build-map.xsl | |
|---|---|
- The entry only exists when the condition holds — easy conditional fields, awkward to do with a literal.
Serialising to a JSON string¶
Once you hold a map or array, serialize() with the right options produces the
JSON text:
| serialize.xsl | |
|---|---|
- The second argument is a serialisation-parameters map.
'method': 'json'is what makes it JSON rather than XML;'indent'pretty-prints it.
Or let the whole result be JSON
Instead of serialize, you can declare <xsl:output method="json"/> and make
JSON the result document itself — then an xsl:sequence of your map at the
top level is serialised to JSON automatically, exactly as method="xml"
serialises a node tree.
The XML-representation route¶
The second approach skips maps entirely. json-to-xml($string) converts JSON
into a regular XML tree in a standard W3C vocabulary — <map>, <array>,
<string>, <number>, <boolean>, <null>, each carrying a key attribute
when it sits inside an object. The point: now all your existing XPath and template
machinery works on JSON.
| json-to-xml.xsl | |
|---|---|
$treeis an ordinary document node in thexpath-functionsnamespace.- Plain XPath against it — find the
stringelement whosekeyisartist. (*:stringignores the namespace prefix for brevity.)
The output of json-to-xml for { "artist": "Bob Dylan", "price": 10.90 }:
The inverse, xml-to-json($tree), takes a tree in that same vocabulary and
returns a JSON string — useful when you would rather build the structure as
familiar XML elements and convert at the end:
| xml-to-json.xsl | |
|---|---|
Which route should I use?¶
- Maps and arrays are the natural choice when JSON is data you compute over
— sum a field, look up a key, reshape a payload. The
?syntax is compact and the values are typed. - The XML representation wins when you would rather lean on template rules,
apply-templates, and XPath you already know — or when you need to round-trip JSON through the same pipeline that handles your XML.
They interoperate freely: parse with parse-json, and if a subtree is easier as
XML, there is nothing stopping you from handling the rest with json-to-xml.
A complete round trip¶
Reading the running catalog and emitting JSON, end to end:
- The result document is JSON — no
serializecall needed. - Top-level
xsl:sequenceof a map; withmethod="json"the processor serialises it. - The simple map operator
!applies the right-handmap { }constructor to eachcdin turn, producing one map per disc — the array members.
Next¶
You have now seen XSLT from 1.0 fundamentals to its modern, JSON-aware 3.0 form. To revisit any topic, head back to the Overview.