chris457
chris457

Reputation: 76

XSLT 3.0 Processing JSON: improving a functions-based solution

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

Answers (2)

Martin Honnen
Martin Honnen

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>

Online fiddle.

Upvotes: 1

Nallani Rajeev
Nallani Rajeev

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

Related Questions