Project

Profile

Help

Streamable creation of JSON XML works but similar approach to create JSON with xsl:map/saxon:array doesn't pass streamability analysis

Added by Martin Honnen over 5 years ago

While exploring a way to create JSON with XSLT 3 and streaming I have found a difference between creating the XML representation of JSON defined in the XSLT 3 spec and the direct creation of "JSON" by creating maps with xsl:map and saxon:array; while my first stylesheet passes the streamability analysis the one creating maps/arrays doesn't.

Tested with Saxon 9.9.0.1 in oXygen 21.

Here is a sample stylesheet creating the XML representation of JSON:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet 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="#all"
    expand-text="yes"
    version="3.0">
    
    <xsl:output method="xml" indent="yes"/>
    <xsl:strip-space elements="*"/>
    
    <xsl:mode on-no-match="shallow-skip" streamable="yes" use-accumulators="#all"/>
    
    <xsl:accumulator name="entry-count" as="xs:integer" initial-value="0" streamable="yes">
        <xsl:accumulator-rule match="Report_Entry" select="$value + 1"/>
    </xsl:accumulator>
    
    <xsl:accumulator name="total-amount" as="xs:decimal" initial-value="0" streamable="yes">
        <xsl:accumulator-rule match="Report_Entry/amount/text()" select="$value + xs:decimal(.)"/>
    </xsl:accumulator>
    
    <xsl:template match="/*">
        <fn:map>
            <fn:array key="entries">
                <xsl:apply-templates select="Report_Data/Report_Entry"/>
            </fn:array>
            <fn:number key="entry-count">{accumulator-after('entry-count')}</fn:number>
            <fn:number key="total-amount">{accumulator-after('total-amount')}</fn:number>
        </fn:map>
    </xsl:template>
    
    <xsl:template match="Report_Entry">
        <fn:map>
            <xsl:apply-templates/>
        </fn:map>
    </xsl:template>
    
    <xsl:template match="Report_Entry/company">
        <fn:string key="name">{data()}</fn:string>
    </xsl:template>
    
    <xsl:template match="Report_Entry/customer_id">
        <fn:string key="id">{data()}</fn:string>
    </xsl:template>
    
    <xsl:template match="Report_Entry/amount">
        <fn:number key="amount">{number(.)}</fn:number>
    </xsl:template>
    
</xsl:stylesheet>

And the same approach as I think creating maps/arrays with xsl:map and saxon:array:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:saxon="http://saxon.sf.net/"
    extension-element-prefixes="saxon"
    exclude-result-prefixes="#all"
    version="3.0">
    
    <xsl:output method="json" indent="yes"/>
    
    <xsl:mode streamable="yes" use-accumulators="#all"/>
    
    <xsl:accumulator name="entry-count" as="xs:integer" initial-value="0" streamable="yes">
        <xsl:accumulator-rule match="Report_Entry" select="$value + 1"/>
    </xsl:accumulator>
    
    <xsl:accumulator name="total-amount" as="xs:decimal" initial-value="0" streamable="yes">
        <xsl:accumulator-rule match="Report_Entry/amount/text()" select="$value + xs:decimal(.)"/>
    </xsl:accumulator>
    
    <xsl:template match="/*">
        <xsl:map>
            <xsl:map-entry key="'entries'">
                <saxon:array>
                    <xsl:apply-templates select="Report_Data/Report_Entry"/>
                </saxon:array>
            </xsl:map-entry>
            <xsl:map-entry key="'entry-count'" select="accumulator-after('entry-count')"/>
            <xsl:map-entry key="'total-amount'" select="accumulator-after('total-amount')"/>
        </xsl:map>
    </xsl:template>
    
    <xsl:template match="Report_Entry">
        <xsl:map>
            <xsl:map-entry key="'name'" select="string(company)"/>
            <xsl:map-entry key="'id'" select="string(customer_id)"/>
            <xsl:map-entry key="'amount'" select="xs:decimal(amount)"/>
        </xsl:map>
    </xsl:template>
    
</xsl:stylesheet>

For that stylesheet I get the error message:

Cannot call accumulator-after except during the post-descent phase of a streaming template

for line 28 <xsl:map-entry key="'entry-count'" select="accumulator-after('entry-count')"/>.

As both stylesheets use <xsl:apply-templates select="Report_Data/Report_Entry"/>, is there some way to use the map construction in a streamable way so that the map entries after the apply-templates can use the post-descent accumulator-after values?


Replies (1)

RE: Streamable creation of JSON XML works but similar approach to create JSON with xsl:map/saxon:array doesn't pass streamability analysis - Added by Martin Honnen over 5 years ago

It seems moving the saxon:array creation into a variable

    <xsl:template match="/*">
        <xsl:map>
            <xsl:variable name="entries" as="array(map(xs:string, xs:anyAtomicType))">
                <saxon:array>
                    <xsl:apply-templates select="Report_Data/Report_Entry"/>
                </saxon:array>
            </xsl:variable>
            <xsl:map-entry key="'entries'" select="$entries"/>            
            <xsl:map-entry key="'entry-count'" select="accumulator-after('entry-count')"/>
            <xsl:map-entry key="'total-amount'" select="accumulator-after('total-amount')"/>
        </xsl:map>
    </xsl:template>

solves the problem to pass the streamability analysis.

I wonder whether the JSON result is created in a streamable way "on the fly", as the property "total-amount":10 ends up in the result before the entries property it seems this is not the case and some buffering is done.

It also seems I forgot to show the sample data I used, it is

<?xml version="1.0" encoding="UTF-8"?>
<Root>
   <Report_Data>
      <Report_Entry>
         <amount>1</amount>
         <customer_id>cust_01</customer_id>
         <company>Company 01</company>
      </Report_Entry>
      <Report_Entry>
         <amount>1</amount>
         <customer_id>cust_02</customer_id>
         <company>Company 02</company>
      </Report_Entry>
      <Report_Entry>
         <amount>1</amount>
         <customer_id>cust_03</customer_id>
         <company>Company 03</company>
      </Report_Entry>
      <Report_Entry>
         <amount>1</amount>
         <customer_id>cust_04</customer_id>
         <company>Company 04</company>
      </Report_Entry>
      <Report_Entry>
         <amount>1</amount>
         <customer_id>cust_05</customer_id>
         <company>Company 05</company>
      </Report_Entry>
      <Report_Entry>
         <amount>1</amount>
         <customer_id>cust_06</customer_id>
         <company>Company 06</company>
      </Report_Entry>
      <Report_Entry>
         <amount>1</amount>
         <customer_id>cust_07</customer_id>
         <company>Company 07</company>
      </Report_Entry>
      <Report_Entry>
         <amount>1</amount>
         <customer_id>cust_08</customer_id>
         <company>Company 08</company>
      </Report_Entry>
      <Report_Entry>
         <amount>1</amount>
         <customer_id>cust_09</customer_id>
         <company>Company 09</company>
      </Report_Entry>
      <Report_Entry>
         <amount>1</amount>
         <customer_id>cust_10</customer_id>
         <company>Company 10</company>
      </Report_Entry>
   </Report_Data>
</Root>
    (1-1/1)

    Please register to reply