Reputation: 76
I took @Michael Kay's comment at How to convert JSON to XML using XSLT? to heart when he said:
XSLT 3.0 isn't actually that good at processing JSON using template rules: it can be done, but it isn't very convenient. It's usually more convenient to use functions.
Given a slightly more than just trivial JSON as input, namely:
{
"name": "Alice",
"age": 30,
"children": [
{"name": "Charlie", "age": 5},
{"name": "Daisy", "age": 3}
]
}
I wanted to
The XML result should be:
<olderChildren>
<child name="Charlie" age="15"/>
<child name="Daisy" age="13"/>
</olderChildren>
To compare the newer function-based approach with the traditional template-based approach I created 2 stylesheets to compare their metrics side by side. This is the function-based stylesheet:
<xsl:stylesheet version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:map="http://www.w3.org/2005/xpath-functions/map"
xmlns:array="http://www.w3.org/2005/xpath-functions/array"
exclude-result-prefixes="map array">
<xsl:output method="xml" omit-xml-declaration="yes" indent="yes"/>
<xsl:param name="json-data">
{
"name": "Alice",
"age": 30,
"children": [
{"name": "Charlie", "age": 5},
{"name": "Daisy", "age": 3}
]
}
</xsl:param>
<xsl:variable name="parsed-json" select="parse-json($json-data)"/>
<xsl:template match="/">
<xsl:variable name="children" select="map:get($parsed-json, 'children')"/> <!-- Extract children array -->
<xsl:variable name="older-children" as="array(*)" select="array:for-each($children, function($child) {map:put($child, 'age', map:get($child, 'age') + 10)})"/> <!-- Create array of children whose age is raised by 10 years-->
<xsl:element name="olderChildren"><!-- Output result XML -->
<xsl:for-each select="$older-children?*">
<xsl:element name="child">
<xsl:attribute name="name" select="map:get(., 'name')"/>
<xsl:attribute name="age" select="map:get(., 'age')"/>
</xsl:element>
</xsl:for-each>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
and this is the template-based 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="xml" indent="yes" omit-xml-declaration="yes"/>
<xsl:param name="json">
{
"name": "Alice",
"age": 30,
"children": [
{"name": "Charlie", "age": 5},
{"name": "Daisy", "age": 3}
]
}
</xsl:param>
<xsl:variable name="XMLfromJSON" select="json-to-xml($json)"/>
<xsl:template match="/">
<xsl:apply-templates select="$XMLfromJSON/fn:map/fn:array"/>
</xsl:template>
<xsl:template match="fn:array[@key = 'children']">
<xsl:element name="{fn:concat('older',fn:upper-case(fn:substring(@key,1,1)),fn:substring(@key,2))}">
<xsl:apply-templates/>
</xsl:element>
</xsl:template>
<xsl:template match="fn:array[@key = 'children']/fn:map">
<xsl:element name="{fn:substring(../@key,1,5)}">
<xsl:attribute name="{./fn:string/@key}" select="./fn:string"/>
<xsl:attribute name="{./fn:number/@key}" select="./fn:number + 10"/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
How can I convert my functional solution to have the structure of the template-based solution, i.e. with apply-templates while maintaining the possibility to derive result XML element names with expressions the ways I did in the template-based solution (i.e. with substring()
etc.)?
One more technical background question: In the function-based solution the contents of <xsl:variable name="older-children" />
was obviously created by creating a deep copy of <xsl:variable name="children">
before processing the new copy (by adding 10 to the ages within all child-maps). One can see that when replacing <xsl:for-each select="$older-children?*">
with <xsl:for-each select="$children?*">
during XML result generation. Then the result is:
<olderChildren>
<child name="Charlie" age="5"/>
<child name="Daisy" age="3"/>
</olderChildren>
, i.e. the ages are the original values.
The select-expression of <xsl:variable name="older-children" />
however seems to reference the maps inside the $children
-variable, at least when looking at the code at face-value: That function($child)
which is called on every $child
of that iteration seems to overwrite field 'age'
in that current $child
of $children
, NOT a newly created deep copy of $children
(select="array:for-each($children, function($child) {map:put($child, 'age', map:get($child, 'age') + 10)}
). Where in the XSLT 3.0 spec does it say that such a deep copy is what is happening?
Upvotes: -2
Views: 106
Reputation: 167716
Maps in XPath 3.1/XDM 3.1/XSLT 3.0 are immutable so any map:put
never manipulates the original map, rather it returns a new map with the changed property.
https://www.w3.org/TR/xpath-functions-31/#map-functions:
As with all other values, the functions in this specification treat maps as immutable. For example, the map:remove function returns a map that differs from the supplied map by the omission (typically) of one entry, but the supplied map is not changed by the operation. Two calls on map:remove with the same arguments return maps that are indistinguishable from each other; there is no way of asking whether these are "the same map".
As for achieving the wanted output
<olderChildren>
<child name="Charlie" age="15"/>
<child name="Daisy" age="13"/>
</olderChildren>
with XSLT 3 processing the JSON with parse-json
, I would do e.g.
<xsl:stylesheet version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:map="http://www.w3.org/2005/xpath-functions/map"
xmlns:array="http://www.w3.org/2005/xpath-functions/array"
exclude-result-prefixes="#all">
<xsl:output method="xml" omit-xml-declaration="yes" indent="yes"/>
<xsl:param name="json-data">
{
"name": "Alice",
"age": 30,
"children": [
{"name": "Charlie", "age": 5},
{"name": "Daisy", "age": 3}
]
}
</xsl:param>
<xsl:variable name="parsed-json" select="parse-json($json-data)"/>
<xsl:template match="/" name="xsl:initial-template">
<olderChildren>
<xsl:for-each select="$parsed-json?children?*!map:put(., 'age', ?age + 10)">
<child name="{?name}" age="{?age}"/>
</xsl:for-each>
</olderChildren>
</xsl:template>
</xsl:stylesheet>
Upvotes: 1
Reputation: 1
the ages are the original values. The select-expression of <xsl:variable name="older-children" />
however seems to reference the maps inside the $children-variable
, at least when looking at the code at face-value:
Upvotes: 0