jsonxmlxsltxslt-3.0

XSLT 3.0 processing JSON to XML: template for array never called - precedence rules unclear


In XSLT 3.0, I want to process in a standard way the XML that is inplicitly produced by a call to json-to-xml() from a JSON input that contains objects and an array of simple values. The solutions given in Convert JSON to XML using XSLT 3.0 functions and JSON to XML transformation in XSLT 3.0 seem simple enough. However the templates for the array are not being called. This is despite the fact, that, to take the 2nd solution as an example, its array-template rules' match conditions, i.e.

<xsl:template match="array">

and

<xsl:template match="array[@key]/*">

are more specific than the first template rule's, i.e <xsl:template match="*[@key]">. I seem to recall that the general rule of thumb for XSLT's precedence rules is:"The more specific rule wins over the more genral rule."

To give an example of my own which is inspired by the above solutions, in the following stylesheet neither the template <xsl:template match="array[@key='phones']"> nor template <xsl:template match="array[@key]"> are ever called:

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

   <xsl:param name="json">
   { 
    "person":{
        "name": "John",
        "age": 30,
        "address": {
          "city": "New York",
          "zipcode": "10001"
        },
        "phones": ["123-4567", "987-6543"]
      }
   }

      <!-- A reminder of what would be produced by a call to <xsl:copy-of select="$XMLfromJSON" />:
<map xmlns="http://www.w3.org/2005/xpath-functions">
    <map key="person">
        <string key="name">John</string>
        <number key="age">30</number>
        <map key="address">
            <string key="city">New York</string>
            <string key="zipcode">10001</string>
        </map>
        <array key="phones">
            <string>123-4567</string>
            <string>987-6543</string>
        </array>
    </map>
</map>
-->
</xsl:param>
    <xsl:variable name="XMLfromJSON" select="json-to-xml($json)" />
   <xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>

   <xsl:template match="/">
        <xsl:apply-templates select="$XMLfromJSON/*"/>
   </xsl:template>

    <!-- Universal template for processing elements with an attribute "key"-->
    <xsl:template match="*[@key]">
        <xsl:element name="{@key}">
            <xsl:apply-templates/>
        </xsl:element>
    </xsl:template>

    <!-- Specific template for processing arrays named "phones" -->
    <xsl:template match="array[@key='phones']">
        <xsl:element name="{@key}">
            <xsl:for-each select="*">
                <value>
                    <xsl:value-of select="."/>
                </value>
            </xsl:for-each>
        </xsl:element>
    </xsl:template>

    <!-- Universal fallback-template for processing other arrays -->
    <xsl:template match="array[@key]">
        <xsl:element name="{@key}">
            <xsl:for-each select="*">
                <item>
                    <xsl:apply-templates/>
                </item>
            </xsl:for-each>
        </xsl:element>
    </xsl:template>
    
</xsl:stylesheet>

This only produces the following XML:

<person>
    <name>John</name>
    <age>30</age>
    <address>
        <city>New York</city>
        <zipcode>10001</zipcode>
    </address>
    <phones>123-4567987-6543</phones>
</person>

What I would like however is the following XML:

<person>
    <name>John</name>
    <age>30</age>
    <address>
        <city>New York</city>
        <zipcode>10001</zipcode>
    </address>
    <phones>
        <value>123-4567</value>
        <value>987-6543</value>
    </phones>
</person>

So my first question is: Why are the array-templates not being called? Can anyone explain it to me? Perhaps with reference to the specific subrule of the XSLT 3.0 precedence rules?

My second question then is: How can I make these templates get automatically called?

If this is not possible my third question would be: What is the most elegant (specifically modular) solution here?

I came up with a solution which produced the desired output, which is:

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

   <xsl:param name="json">
   { 
    "person":{
        "name": "John",
        "age": 30,
        "address": {
          "city": "New York",
          "zipcode": "10001"
        },
        "phones": ["123-4567", "987-6543"]
      }
   }

      
</xsl:param>
    <xsl:variable name="XMLfromJSON" select="json-to-xml($json)" />
   <xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>

   <xsl:template match="/">
        <xsl:apply-templates select="$XMLfromJSON/*"/>
   </xsl:template>

    <xsl:template match="*[@key]" >
        <xsl:element name="{@key}">
            <xsl:choose>
                <xsl:when test="name() = 'array'">
                    <xsl:call-template name="process-array"/>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:apply-templates/>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:element>
    </xsl:template>
    
    <!-- Named template for processing arrays -->
    <xsl:template name="process-array">
        <xsl:for-each select="*">
            <value>
                <xsl:apply-templates/>
            </value>
        </xsl:for-each>
    </xsl:template>
    
</xsl:stylesheet>

However, I really would like to avoid any conditional code like xsl:choose or xsl:if and named templates and instead rely on the XSLT's processors rules.

As an optional 4th question and out of interest: The first link I cited above, i.e. Convert JSON to XML using XSLT 3.0 functions

used a <xsl:copy>-element like so:

<xsl:copy>
        <xsl:apply-templates select="json-to-xml(.)/*"/>
</xsl:copy>

However, I removed it and it does the same. The <xsl:copy>-element also does not appear in the very similar solution of the 2nd link above. Is there a purpose for this <xsl:copy>-element? Perhaps under specific circumstances? Or is it just superfluous?


Solution

  • With xmlns:fn="http://www.w3.org/2005/xpath-functions" declared, you can match on fn:array. If you want your unqualified match="array" to match the elements in the XML representation of JSON (the elements are in a namespace), then you can use xpath-default-namespace="http://www.w3.org/2005/xpath-functions" in your XSLT.