XSLT output JSON instead of XML - json

I have a XSLT that works the way I want when it outputs XML, however I would like to change the XSLT it to output JSON instead. The problem appears to me with the inner most for-each loop, but I'm not sure. If there is way to make this more efficient I'm also interested in your suggestions.
Sample input XML
<?xml version="1.0" encoding="UTF-8"?>
<root xmlns:xs="http://www.w3.org/2001/XMLSchema">
<Metric measType="1526727075"
measResult="0"
endTime="2016-08-25T04:30:00-07:00"
measObjLdn="LTHBC0126858/GTPU:Board Type=MPT, Cabinet No.=0, Subrack No.=1, Slot No.=7"
Element_Type="ENODEB"
Key1="LTHBC0126858"
TableName="HH_ENODEB"
ColumnName="H1526727075"
H1526727075="0"/>
<Metric measType="1526727076"
measResult="0"
endTime="2016-08-25T04:30:00-07:00"
measObjLdn="LTHBC0126858/GTPU:Board Type=MPT, Cabinet No.=0, Subrack No.=1, Slot No.=7"
Element_Type="ENODEB"
Key1="LTHBC0126858"
TableName="HH_ENODEB"
ColumnName="H1526727076"
H1526727076="0"/>
<Metric measType="1526727077"
measResult="0"
endTime="2016-08-25T04:30:00-07:00"
measObjLdn="LTHBC0126858/GTPU:Board Type=MPT, Cabinet No.=0, Subrack No.=1, Slot No.=7"
Element_Type="ENODEB"
Key1="LTHBC0126858"
TableName="HH_ENODEB"
ColumnName="H1526727077"
H1526727077="0"/>
</root>
This is the XSLT that outputs XML and works the way I expect
<?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" version="2.0">
<xsl:output method="xml" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="root">
<root>
<xsl:for-each-group select="Metric" group-by="#measObjLdn">
<xsl:sort select="current-grouping-key()"/>
<xsl:variable name="curr_key" select="current-grouping-key()"/>
<xsl:for-each-group select="current-group()" group-by="#TableName">
<xsl:sort select="current-grouping-key()"/>
<xsl:if test="current-grouping-key() != ''">
<Table TableName="{current-grouping-key()}">
<xsl:for-each select="current-group()">
<xsl:attribute name="Stamp">
<xsl:value-of select="#endTime"/>
</xsl:attribute>
<xsl:attribute name="measObjLdn">
<xsl:value-of select="$curr_key"/>
</xsl:attribute>
<xsl:attribute name="Element_Type">
<xsl:value-of select="#Element_Type"/>
</xsl:attribute>
<xsl:attribute name="Key1">
<xsl:value-of select="#Key1"/>
</xsl:attribute>
<xsl:for-each select="#*">
<xsl:if test="starts-with(name(), 'H')">
<xsl:attribute name="{name()}">
<xsl:value-of select="number(.)"/>
</xsl:attribute>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
</Table>
</xsl:if>
</xsl:for-each-group>
</xsl:for-each-group>
</root>
</xsl:template>
</xsl:stylesheet>
This is my JSON output code, but not working as expected.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="2.0">
<xsl:output method="text" encoding="utf-8"/>
<xsl:strip-space elements="*"/>
<xsl:template match="root">
<xsl:text>{"root":{</xsl:text>
<xsl:for-each-group select="Metric" group-by="#TableName">
<xsl:sort select="current-grouping-key()"/>
<xsl:if test="current-grouping-key() != ''">
<xsl:text>"Table":[</xsl:text>
<xsl:text>{"TableName":"</xsl:text>
<xsl:value-of select="current-grouping-key()"/>
<!--<Table TableName="{current-grouping-key()}">-->
<xsl:text>",</xsl:text>
<xsl:for-each-group select="current-group()" group-by="#measObjLdn">
<xsl:sort select="current-grouping-key()"/>
<xsl:for-each select="current-group()">
<xsl:text>"Stamp":"</xsl:text>
<xsl:value-of select="#endTime"/>
<xsl:text>",</xsl:text>
<xsl:text>"measObjLdn":"</xsl:text>
<xsl:value-of select="current-grouping-key()"/>
<xsl:text>",</xsl:text>
<xsl:text>"Element_Type":"</xsl:text>
<xsl:value-of select="#Element_Type"/>
<xsl:text>",</xsl:text>
<xsl:text>"Key1":"</xsl:text>
<xsl:value-of select="#Key1"/>
<xsl:text>",</xsl:text>
<xsl:for-each select="#attribute()">
<xsl:if test="starts-with(name(), 'H')">
<xsl:text>"</xsl:text>
<xsl:value-of select="name()"/>
<xsl:text>":"</xsl:text>
<xsl:value-of select="number(.)"/>
<xsl:text>"</xsl:text>
<xsl:if test="position() != last()">
<xsl:text>,</xsl:text>
</xsl:if>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
</xsl:for-each-group>
<!--</Table>-->
<xsl:text>}</xsl:text>
<xsl:if test="position() != last()">
<xsl:text>,</xsl:text>
</xsl:if>
</xsl:if>
</xsl:for-each-group>
<xsl:text>}</xsl:text>
</xsl:template>
</xsl:stylesheet>
Good point, so the XML version groups all the attributes for the same table together; Given the sample above the XML output is as follows (some reformatting applied for clarity):
<root xmlns:xs="http://www.w3.org/2001/XMLSchema">
<Table
TableName="HH_ENODEB"
Stamp="2016-08-25T04:30:00-07:00"
measObjLdn="LTHBC0126858/GTPU:Board Type=MPT, Cabinet No.=0, Subrack No.=1, Slot No.=7"
Element_Type="ENODEB"
Key1="LTHBC0126858"
H1526727075="0"
H1526727076="0"
H1526727077="0"/>
</root>
JSON example output is not a JSON version of the XML version which I expected (some whitespace reformatting applied for clarity).
{
"root": {
"Table": [
{
"TableName":"HH_ENODEB",
"Stamp":"2016-08-25T04:30:00-07:00",
"measObjLdn":"LTHBC0126858/GTPU:Board Type=MPT, Cabinet No.=0, Subrack No.=1, Slot No.=7",
"Element_Type":"ENODEB",
"Key1":"LTHBC0126858",
"H1526727075":"0"
"Stamp":"2016-08-25T04:30:00-07:00",
"measObjLdn":"LTHBC0126858/GTPU:Board Type=MPT, Cabinet No.=0, Subrack No.=1, Slot No.=7",
"Element_Type":"ENODEB",
"Key1":"LTHBC0126858",
"H1526727076":"0"
"Stamp":"2016-08-25T04:30:00-07:00",
"measObjLdn":"LTHBC0126858/GTPU:Board Type=MPT, Cabinet No.=0, Subrack No.=1, Slot No.=7",
"Element_Type":"ENODEB",
"Key1":"LTHBC0126858",
"H1526727077":"0"
}}
(The above is not even well-formed JSON)

You didn't actually say what your expected JSON should look like, but based on your XML, I am assuming it should look like this
{
"root":
{
"Table":
[
{
"TableName":"HH_ENODEB",
"Stamp":"2016-08-25T04:30:00-07:00",
"measObjLdn":"HH_ENODEB",
"Element_Type":"ENODEB",
"Key1":"LTHBC0126858",
"H1526727075":"0",
"H1526727076":"0",
"H1526727077":"0"
}
]
}
}
The issue is that in the XSLT that produces the XML output, you have the creation of the xsl:attributes within the statement <xsl:for-each select="current-group()">, but that means you are repeatedly outputting the same attribute names for a single Table element. In this case, XSLT will just replace any existing attribute with the latest one created, so you don't notice what is going on.
When outputting JSON (or rather, when outputting text, which just happens to be in JSON format), you do end up with the repeated attributes as it is just text.
The solution is to move the creation of the main attributes outside the inner loop.
Try this XSLT
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" version="2.0">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:template match="root">
<xsl:text>{
"root":
{
</xsl:text>
<xsl:for-each-group select="Metric" group-by="#measObjLdn">
<xsl:sort select="current-grouping-key()"/>
<xsl:variable name="curr_key" select="current-grouping-key()"/>
<xsl:text> "Table":
[
</xsl:text>
<xsl:for-each-group select="current-group()" group-by="#TableName">
<xsl:sort select="current-grouping-key()"/>
<xsl:if test="current-grouping-key() != ''">
<xsl:text> {
"TableName":"</xsl:text>
<xsl:value-of select="current-grouping-key()"/>
<xsl:text>",
</xsl:text>
<xsl:text> "Stamp":"</xsl:text>
<xsl:value-of select="#endTime"/>
<xsl:text>",
</xsl:text>
<xsl:text> "measObjLdn":"</xsl:text>
<xsl:value-of select="$curr_key"/>
<xsl:text>",
</xsl:text>
<xsl:text> "Element_Type":"</xsl:text>
<xsl:value-of select="#Element_Type"/>
<xsl:text>",
</xsl:text>
<xsl:text> "Key1":"</xsl:text>
<xsl:value-of select="#Key1"/>
<xsl:text>"</xsl:text>
<xsl:for-each select="current-group()">
<xsl:for-each select="#*[starts-with(name(), 'H')]">
<xsl:text>,
</xsl:text>
<xsl:text> "</xsl:text>
<xsl:value-of select="name()"/>
<xsl:text>":"</xsl:text>
<xsl:value-of select="number(.)"/>
<xsl:text>"</xsl:text>
</xsl:for-each>
</xsl:for-each>
<xsl:text>
}
</xsl:text>
</xsl:if>
</xsl:for-each-group>
<xsl:text> ]
</xsl:text>
</xsl:for-each-group>
<xsl:text> }
}</xsl:text>
</xsl:template>
</xsl:stylesheet>

Related

Converting XML to CSV using XSLT Formatting Issue

I have the following XML
<Data>
<Employee>
<Name>
<FirstName>John</FirstName>
<LastName>Snow</LastName>
</Name>
<DOB>1990-01-01</DOB>
<Passport>
<Country>United Kingdom</Country>
<ID>12345678</ID>
</Passport>
<Passport>
<Country>United States of America</Country>
<ID>789101112</ID>
</Passport>
</Employee>
</Data>
And I am converting it as seen below:
<xsl:for-each select="Data/Employee">
<xsl:value-of select="Name/FirstName"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="Name/LastName"/>
<xsl:text>,</xsl:text>
<xsl:for-each select="Passport">
<xsl:value-of select="Country"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="ID"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
<xsl:text>,</xsl:text>
<xsl:value-of select="DOB"/>
</xsl:for-each>
The output I am getting is:
John,Snow,United Kingdom,12345678
United States of America,789101112
,1990-01-01
But I require the passport information to be populated below each other and then DOB to be shown on the top line as seen below:
John,Snow,United Kingdom,12345678,1990-01-01
,,United States of America,789101112
Any help would be appreciated
Perhaps like this:
XSLT 2.0
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >
<xsl:output method="text" encoding="UTF-8"/>
<xsl:template match="/Data">
<xsl:for-each select="Employee">
<xsl:for-each select="Passport">
<xsl:choose>
<xsl:when test="position()=1">
<xsl:value-of select="../Name/(FirstName, LastName), Country,ID, ../DOB" separator=","/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="'', '', Country,ID, ''" separator=","/>
</xsl:otherwise>
</xsl:choose>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Note that I have added a trailing comma in the place of the "missing" DOB.

Handle excluded element in a foreach

In first template, I am intentionally excluding an element ('milk') because the parsed data map is relatively flat and I would like to use XSLT to categorize and structure the data. The aim is to process the excluded element ('milk') in the second template. The both templates works running them one at a time. Running the templates together will not show the result of the excluded element ('milk') which should set another attribute name and attribute value.
JSON:
<data>
{
"storage": {
"pencils": 12,
"milk": 8,
"rulers": 4
}
}
</data>
XSL:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:transform
version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:storage="http://www.exammple.com/1"
xmlns:office="http://www.exammple.com/2"
xmlns:item="http://www.exammple.com/3"
expand-text="yes">
<xsl:output method="xml" indent="yes"/>
<xsl:mode on-no-match="shallow-skip"/>
<!-- Parse JSON to XML -->
<xsl:template match="data">
<storage:one>
<xsl:apply-templates select="json-to-xml(.)"/>
</storage:one>
</xsl:template>
<!-- Print map -->
<!-- <xsl:template match="*[#key = 'storage']"> <xsl:copy-of select=".."/> </xsl:template> -->
<xsl:template match="*[#key='storage']">
<xsl:for-each select="*[not(#key='milk')]">
<xsl:element name="item:{#key}">
<xsl:attribute name="office">plant-1</xsl:attribute>
<xsl:value-of select="text()"/>
</xsl:element>
</xsl:for-each>
</xsl:template>
<xsl:template match="*[#key='milk']">
<xsl:for-each select=".">
<xsl:element name="item:{#key}">
<xsl:attribute name="beverage">plant-2</xsl:attribute>
<xsl:value-of select="text()"/>
</xsl:element>
</xsl:for-each>
</xsl:template>
</xsl:transform>
Result:
<?xml version="1.0" encoding="UTF-8"?>
<storage:one xmlns:item="http://www.exammple.com/3"
xmlns:office="http://www.exammple.com/2"
xmlns:storage="http://www.exammple.com/1">
<item:pencils office="plant-1">12</item:pencils>
<item:rulers office="plant-1">4</item:rulers>
</storage:one>
Wanted result:
<?xml version="1.0" encoding="UTF-8"?>
<storage:one xmlns:item="http://www.exammple.com/3"
xmlns:office="http://www.exammple.com/2"
xmlns:storage="http://www.exammple.com/1">
<item:pencils office="plant-1">12</item:pencils>
<item:rulers office="plant-1">4</item:rulers>
<item:milk beverage="plant-2">8</item:milk>
</storage:one>
Your second template is never matched, because it is never reached. All elements are processed by <xsl:template match="*[#key='storage']"> - which doesn't have an <xsl:apply-templates ...> to reach further templates.
Your first template does not recurse into its children. So add an <xsl:apply-templates select="*" /> to the end of the first template:
<xsl:template match="*[#key='storage']">
<xsl:for-each select="*[not(#key='milk')]">
<xsl:element name="item:{#key}">
<xsl:attribute name="office">plant-1</xsl:attribute>
<xsl:value-of select="text()"/>
</xsl:element>
</xsl:for-each>
<xsl:apply-templates select="*" />
</xsl:template>
This will try to apply further templates at the level of "storage" and therefore match the second template.
I would write templates for each different output type and if the order of the output is different from the order of the input throw in an xsl:sort or an XPath 3.1 sort call to change the order:
<xsl:template match="data">
<storage:one>
<xsl:apply-templates select="json-to-xml(.)"/>
</storage:one>
</xsl:template>
<xsl:template match="*[#key = 'storage']">
<xsl:apply-templates select="sort(*, (), function($el) { $el/#key = 'milk' })"/>
</xsl:template>
<xsl:template match="*[#key='storage']/*[not(#key='milk')]">
<xsl:element name="item:{#key}">
<xsl:attribute name="office">plant-1</xsl:attribute>
<xsl:value-of select="text()"/>
</xsl:element>
</xsl:template>
<xsl:template match="*[#key='storage']/*[#key='milk']">
<xsl:element name="item:{#key}">
<xsl:attribute name="beverage">plant-2</xsl:attribute>
<xsl:value-of select="text()"/>
</xsl:element>
</xsl:template>

Tranform XML to JSON using XSLT and remove a specific pair name in the JSON

I have an xml that needs to be converted to JSON and am using XSLT to transform it. In one of the element am not required to pass pair name, only values. See below
My XML:
<?xml version="1.0" encoding="UTF-8"?>
<map xmlns="http://www.w3.org/2005/xpath-functions">
<array key="cars">
<map>
<string key="doors">4</string>
<string key="price">6L</string>
</map>
<map>
<string key="doors">5</string>
<string key="price">13L</string>
</map>
</array>
</map>
Using XSL: https://xsltfiddle.liberty-development.net/b4GWVd
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="3.0">
<xsl:output omit-xml-declaration="yes"/>
<xsl:template match="/">
<xsl:value-of select="translate(xml-to-json(., map { 'indent' : true() }),'Ø', 'ø')"/>
</xsl:template>
</xsl:stylesheet>
Gives output: Which is correct
{ "cars" :
[
{ "doors" : "4",
"price" : "6L" },
{ "doors" : "5",
"price" : "13L" } ] }
But then I would like to get a JSON with below structure, Without Pair Name(Reason: Its the structure required to be submitted to an API)
{ "cars" :
[
{ "4",
"6L" },
{ "5",
"13L" } ] }
As the XSLT 3.0 spec also provides an implementation of xml-to-json as an XSLT 3.0 package you could use that code and override templates where you want to eliminate the "keys" of JSON objects/XDM map items:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:j="http://www.w3.org/2013/XSLT/xml-to-json"
xmlns:fn="http://www.w3.org/2005/xpath-functions"
exclude-result-prefixes="#all"
version="3.0">
<xsl:use-package name="http://www.w3.org/2013/XSLT/xml-to-json" package-version="1.0">
<xsl:override>
<!-- Template rule for fn:map elements, representing JSON objects , overridden for content of cars array -->
<xsl:template match="fn:array[#key = 'cars']/fn:map" mode="indent">
<xsl:value-of>
<xsl:variable name="depth" select="count(ancestor::*) + 1"/>
<xsl:text>{</xsl:text>
<xsl:for-each select="*">
<xsl:if test="position() gt 1">
<xsl:text>, </xsl:text>
<xsl:value-of select="j:indent($depth)"/>
</xsl:if>
<!--<xsl:apply-templates select="snapshot(#key)" mode="key-attribute"/>
<xsl:text> : </xsl:text>-->
<xsl:apply-templates select="." mode="#current"/>
</xsl:for-each>
<xsl:text>}</xsl:text>
</xsl:value-of>
</xsl:template>
<xsl:template match="fn:array[#key = 'cars']/fn:map/*" mode="no-indent">
<xsl:value-of>
<xsl:text>{</xsl:text>
<xsl:for-each select="*">
<xsl:if test="position() gt 1">
<xsl:text>,</xsl:text>
</xsl:if>
<!--<xsl:apply-templates select="snapshot(#key)" mode="key-attribute"/>
<xsl:text>:</xsl:text>-->
<xsl:apply-templates select="." mode="#current"/>
</xsl:for-each>
<xsl:text>}</xsl:text>
</xsl:value-of>
</xsl:template>
</xsl:override>
</xsl:use-package>
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:value-of select="j:xml-to-json(., map { 'indent' : true() })"/>
</xsl:template>
</xsl:stylesheet>
The above can be run with Saxon 10 HE and higher or Saxon 9.8 PE or EE and later by using the command line -s:your-xml.xml -xsl:above-xslt-xsl -lib:w3c-xml-to-json.xsl where the last option refers to the file https://www.w3.org/TR/xslt-30/xml-to-json.xsl linked from the XSLT 3 spec with one path/errata added, namely the declaration of the default mode with <xsl:mode name="j:xml-to-json"/> so it would look like
<xsl:package
name="http://www.w3.org/2013/XSLT/xml-to-json"
package-version="1.0"
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"
xmlns:j="http://www.w3.org/2013/XSLT/xml-to-json"
exclude-result-prefixes="xs fn j" default-mode="j:xml-to-json" version="3.0">
<xsl:variable name="quot" visibility="private">"</xsl:variable>
<xsl:param name="indent-spaces" select="2"/>
<!-- The static parameter STREAMABLE controls whether the stylesheet is declared as streamable -->
<xsl:param name="STREAMABLE" static="yes" as="xs:boolean" select="true()"/>
<!-- fix for https://github.com/w3c/qtspecs/blob/master/errata/xslt-30/errata.xml#L1154 -->
<xsl:mode name="j:xml-to-json"/>
<xsl:mode name="indent" _streamable="{$STREAMABLE}" visibility="public"/>
<xsl:mode name="no-indent" _streamable="{$STREAMABLE}" visibility="public"/>
<xsl:mode name="key-attribute" streamable="false" on-no-match="fail" visibility="public"/>
<!-- The static parameter VALIDATE controls whether the input, if untyped, should be validated -->
<xsl:param name="VALIDATE" static="yes" as="xs:boolean" select="false()"/>
<xsl:import-schema namespace="http://www.w3.org/2005/xpath-functions" use-when="$VALIDATE"/>
<!-- Entry point: function to convert a supplied XML node to a JSON string -->
<xsl:function name="j:xml-to-json" as="xs:string" visibility="public">
<xsl:param name="input" as="node()"/>
<xsl:sequence select="j:xml-to-json($input, map{})"/>
</xsl:function>
<!-- Entry point: function to convert a supplied XML node to a JSON string, supplying options -->
<xsl:function name="j:xml-to-json" as="xs:string" visibility="public">
<xsl:param name="input" as="node()"/>
<xsl:param name="options" as="map(*)"/>
<xsl:variable name="input" as="node()" use-when="$VALIDATE">
<xsl:copy-of select="$input" validation="strict"/>
</xsl:variable>
<xsl:choose>
<xsl:when test="$options('indent') eq true()">
<xsl:apply-templates select="$input" mode="indent">
<xsl:with-param name="fallback" as="(function(element()) as xs:string)?"
select="$options('fallback')" tunnel="yes"/>
</xsl:apply-templates>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="$input" mode="no-indent">
<xsl:with-param name="fallback" as="(function(element()) as xs:string)?"
select="$options('fallback')" tunnel="yes"/>
</xsl:apply-templates>
</xsl:otherwise>
</xsl:choose>
</xsl:function>
<!-- A document node is ignored -->
<xsl:template match="/" mode="indent no-indent">
<xsl:apply-templates mode="#current"/>
</xsl:template>
<!-- Template rule for fn:map elements, representing JSON objects -->
<xsl:template match="fn:map" mode="indent">
<xsl:value-of>
<xsl:variable name="depth" select="count(ancestor::*) + 1"/>
<xsl:text>{</xsl:text>
<xsl:for-each select="*">
<xsl:if test="position() gt 1">
<xsl:text>, </xsl:text>
<xsl:value-of select="j:indent($depth)"/>
</xsl:if>
<xsl:apply-templates select="snapshot(#key)" mode="key-attribute"/>
<xsl:text> : </xsl:text>
<xsl:apply-templates select="." mode="#current"/>
</xsl:for-each>
<xsl:text>}</xsl:text>
</xsl:value-of>
</xsl:template>
<xsl:template match="fn:map" mode="no-indent">
<xsl:value-of>
<xsl:text>{</xsl:text>
<xsl:for-each select="*">
<xsl:if test="position() gt 1">
<xsl:text>,</xsl:text>
</xsl:if>
<xsl:apply-templates select="snapshot(#key)" mode="key-attribute"/>
<xsl:text>:</xsl:text>
<xsl:apply-templates select="." mode="#current"/>
</xsl:for-each>
<xsl:text>}</xsl:text>
</xsl:value-of>
</xsl:template>
<!-- Template rule for fn:array elements, representing JSON arrays -->
<xsl:template match="fn:array" mode="indent">
<xsl:value-of>
<xsl:variable name="depth" select="count(ancestor::*) + 1"/>
<xsl:text>[</xsl:text>
<xsl:for-each select="*">
<xsl:if test="position() gt 1">
<xsl:text>, </xsl:text>
<xsl:value-of select="j:indent($depth)"/>
</xsl:if>
<xsl:apply-templates select="." mode="#current"/>
</xsl:for-each>
<xsl:text>]</xsl:text>
</xsl:value-of>
</xsl:template>
<xsl:template match="fn:array" mode="no-indent">
<xsl:value-of>
<xsl:text>[</xsl:text>
<xsl:for-each select="*">
<xsl:if test="position() gt 1">
<xsl:text>,</xsl:text>
</xsl:if>
<xsl:apply-templates select="." mode="#current"/>
</xsl:for-each>
<xsl:text>]</xsl:text>
</xsl:value-of>
</xsl:template>
<!-- Template rule for fn:string elements in which
special characters are already escaped -->
<xsl:template match="fn:string[#escaped='true']" mode="indent no-indent">
<xsl:sequence select="concat($quot, ., $quot)"/>
</xsl:template>
<!-- Template rule for fn:string elements in which
special characters need to be escaped -->
<xsl:template match="fn:string[not(#escaped='true')]" mode="indent no-indent">
<xsl:sequence select="concat($quot, j:escape(.), $quot)"/>
</xsl:template>
<!-- Template rule for fn:boolean elements -->
<xsl:template match="fn:boolean" mode="indent no-indent">
<xsl:sequence select="xs:string(xs:boolean(.))"/>
</xsl:template>
<!-- Template rule for fn:number elements -->
<xsl:template match="fn:number" mode="indent no-indent">
<xsl:value-of select="xs:string(xs:double(.))"/>
</xsl:template>
<!-- Template rule for JSON null elements -->
<xsl:template match="fn:null" mode="indent no-indent">
<xsl:text>null</xsl:text>
</xsl:template>
<!-- Template rule matching a key within a map where
special characters in the key are already escaped -->
<xsl:template match="fn:*[#key-escaped='true']/#key" mode="key-attribute">
<xsl:value-of select="concat($quot, ., $quot)"/>
</xsl:template>
<!-- Template rule matching a key within a map where
special characters in the key need to be escaped -->
<xsl:template match="fn:*[not(#key-escaped='true')]/#key" mode="key-attribute">
<xsl:value-of select="concat($quot, j:escape(.), $quot)"/>
</xsl:template>
<!-- Template matching "invalid" elements -->
<xsl:template match="*" mode="indent no-indent">
<xsl:param name="fallback" as="(function(element()) as xs:string)?"
tunnel="yes" required="yes"/>
<xsl:choose>
<xsl:when test="exists($fallback)">
<xsl:value-of select="$fallback(snapshot(.))"/>
</xsl:when>
<xsl:otherwise>
<xsl:message terminate="yes">>Inc</xsl:message>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- Template rule matching (and discarding) whitespace text nodes in the XML -->
<xsl:template match="text()[not(normalize-space())]" mode="indent no-indent"/>
<!-- Function to escape special characters -->
<xsl:function name="j:escape" as="xs:string" visibility="final">
<xsl:param name="in" as="xs:string"/>
<xsl:value-of>
<xsl:for-each select="string-to-codepoints($in)">
<xsl:choose>
<xsl:when test=". gt 65535">
<xsl:value-of select="concat('\u', j:hex4((. - 65536) idiv 1024 + 55296))"/>
<xsl:value-of select="concat('\u', j:hex4((. - 65536) mod 1024 + 56320))"/>
</xsl:when>
<xsl:when test=". = 34">\"</xsl:when>
<xsl:when test=". = 92">\\</xsl:when>
<xsl:when test=". = 08">\b</xsl:when>
<xsl:when test=". = 09">\t</xsl:when>
<xsl:when test=". = 10">\n</xsl:when>
<xsl:when test=". = 12">\f</xsl:when>
<xsl:when test=". = 13">\r</xsl:when>
<xsl:when test=". lt 32 or (. ge 127 and . le 160)">
<xsl:value-of select="concat('\u', j:hex4(.))"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="codepoints-to-string(.)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:value-of>
</xsl:function>
<!-- Function to convert a UTF16 codepoint into a string of four hex digits -->
<xsl:function name="j:hex4" as="xs:string" visibility="final">
<xsl:param name="ch" as="xs:integer"/>
<xsl:variable name="hex" select="'0123456789abcdef'"/>
<xsl:value-of>
<xsl:value-of select="substring($hex, $ch idiv 4096 + 1, 1)"/>
<xsl:value-of select="substring($hex, $ch idiv 256 mod 16 + 1, 1)"/>
<xsl:value-of select="substring($hex, $ch idiv 16 mod 16 + 1, 1)"/>
<xsl:value-of select="substring($hex, $ch mod 16 + 1, 1)"/>
</xsl:value-of>
</xsl:function>
<!-- Function to output whitespace indentation based on
the depth of the node supplied as a parameter -->
<xsl:function name="j:indent" as="text()" visibility="public">
<xsl:param name="depth" as="xs:integer"/>
<xsl:value-of select="'
', string-join((1 to ($depth + 1) * $indent-spaces) ! ' ', '')"/>
</xsl:function>
</xsl:package>
To run code with Java code you basically need to use an XsltCompiler created from Processor e.g.
Processor processor = new Processor(true);
XsltCompiler xsltCompiler = processor.newXsltCompiler();
XsltPackage xmlToJsonPackage = xsltCompiler.compilePackage(new StreamSource("w3c-xml-to-json.xsl"));
xsltCompiler.importPackage(xmlToJsonPackage);
XsltExecutable xsltExecutable = xsltCompiler.compile(new StreamSource("sheet.xsl"));
Xslt30Transformer xslt30Transformer = xsltExecutable.load30();
// now run stylesheet with e.g. transform() or applyTemplates()
xslt30Transformer.transform(new StreamSource("input.xml"), xslt30Transformer.newSerializer(System.out));

XSLT: convert XML to JSON creating a group of sub nodes

it's been days trying to perform a transformation to get a JSON file from XML documents. My xml doc have different levels of sub nodes, all examples I found on internet don't catch my case.
Here is my xml example:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<foo id="1" group="B" />
<foo id="2" group="A" />
<foo id="3", group="A">
<foo id="4" group="A" />
<foo id="5" group="A">
<foo id="6" group="A" />
<foo id="7" group="A" />
<foo id="8" group="A" />
</foo>
</foo>
<foo id="9" group="A"></foo>
</root>
desired JSON:
{
"B": {
"id": 1
},
"A": {
"id": 2
},
"A": [{
"id": 4
},
{
"A": [{
"id": 6
},
{
"id": 7
},
{
"id": 8
}
]
}
],
"A": {
"id": 9
}
}
Means whenever I have nested <foo> elements, child elements are grouped with the parent element, and so on.
I tried some xsl code (see1 and see2) and failed to make them work for my case.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs"
version="2.0">
<xsl:strip-space elements="*"/>
<xsl:output method="xml" indent="yes"/>
<xsl:template match="root">
<xsl:text>{</xsl:text>
<xsl:apply-templates select="//foo"/>
<xsl:text>}</xsl:text>
</xsl:template>
<xsl:template match="//foo">
<xsl:choose>
<xsl:when test=" count(ancestor::foo) = 1 and child::foo"/>
<xsl:when test="foo[child::foo]"/>
<xsl:otherwise>
<xsl:text>"</xsl:text><xsl:value-of select="#group"/><xsl:text>"</xsl:text>:<xsl:text> {
</xsl:text>
<xsl:text> "</xsl:text><xsl:value-of select="#id/name()"/><xsl:text>"</xsl:text>:<xsl:value-of select=" concat(' ',#id)"/>
<xsl:text>
},</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Please Check this code:
<?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" exclude-result-prefixes="xs" version="2.0">
<xsl:output method="xml" omit-xml-declaration="yes"/>
<xsl:template match="root">
<xsl:text>{</xsl:text>
<xsl:text>
</xsl:text>
<xsl:for-each select="foo">
<xsl:if test="not(child::foo)">
<xsl:text> "</xsl:text><xsl:value-of select="#group"/><xsl:text>": </xsl:text><xsl:text>{</xsl:text><xsl:text>
</xsl:text>
<xsl:text> "id": </xsl:text><xsl:value-of select="#id"/><xsl:text>
</xsl:text>
<xsl:text> }</xsl:text>
<xsl:if test="position()!=last()">
<xsl:text>,</xsl:text>
</xsl:if>
<xsl:text>
</xsl:text>
</xsl:if>
<xsl:for-each select="foo">
<xsl:if test="not(child::foo)">
<xsl:text> "</xsl:text><xsl:value-of select="#group"/><xsl:text>": </xsl:text><xsl:if test="parent::foo"><xsl:text>[</xsl:text></xsl:if><xsl:text>{</xsl:text><xsl:text>
</xsl:text>
<xsl:text> "id": </xsl:text><xsl:value-of select="#id"/><xsl:text>
</xsl:text>
<xsl:text> },</xsl:text><xsl:text>
</xsl:text>
<xsl:text> {</xsl:text><xsl:text>
</xsl:text>
<xsl:text> "</xsl:text><xsl:value-of select="#group"/><xsl:text>": </xsl:text><xsl:text>[</xsl:text><xsl:text>
</xsl:text>
</xsl:if>
<!-- <xsl:text> "</xsl:text><xsl:value-of select="#group"/><xsl:text>": </xsl:text><xsl:text>[{</xsl:text><xsl:text>
</xsl:text>-->
<xsl:for-each select="foo">
<xsl:text> {</xsl:text><xsl:text>
</xsl:text>
<xsl:text> "id": </xsl:text><xsl:value-of select="#id"/><xsl:text>
</xsl:text>
<xsl:text> }</xsl:text>
<xsl:if test="position()!=last()">
<xsl:text>,</xsl:text>
</xsl:if><xsl:text>
</xsl:text>
<xsl:if test="position()=last()">
<xsl:text> ]</xsl:text><xsl:text>
</xsl:text>
</xsl:if>
<xsl:if test="position()=last()">
<xsl:text> }</xsl:text><xsl:text>
</xsl:text>
</xsl:if>
<xsl:if test="position()=last()">
<xsl:text>],</xsl:text><xsl:text>
</xsl:text>
</xsl:if>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:for-each>
</xsl:for-each>
<xsl:text>}</xsl:text>
</xsl:template>
</xsl:stylesheet>
It is not quite clear which result you want as the desired "JSON" has duplicated properties if there are various foo group="A" elements but in general of XML to JSON generation you can use XSLT 3 with support for XPath 3.1 maps and arrays (https://www.w3.org/TR/xpath-31/#id-maps-and-arrays) and the json output method or the xml-to-json function (https://www.w3.org/TR/xslt-30/#json, https://www.w3.org/TR/xslt-30/#func-xml-to-json), that way you have two ways of creating JSON and outputting it, you can either transform your XML directly to XPath 3.1 maps/arrays and serialize them with <xsl:output method="json"/> or you can use the usual XSLT processing to transform your input XML to the JSON XML representation the xml-to-json function expects and apply it to get the JSON as text.
Here is an example of the first approach:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
exclude-result-prefixes="#all"
version="3.0">
<xsl:mode on-no-match="shallow-skip"/>
<xsl:strip-space elements="*"/>
<xsl:output method="json" indent="yes"/>
<xsl:template match="*[*]">
<xsl:map>
<xsl:for-each-group select="foo" group-by="#group">
<xsl:map-entry key="string(current-grouping-key())">
<xsl:choose>
<xsl:when test="not(tail(current-group()))">
<xsl:apply-templates select="current-group()"/>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="group-transform" as="map(*)*">
<xsl:apply-templates select="current-group()"/>
</xsl:variable>
<xsl:sequence select="array { $group-transform }"/>
</xsl:otherwise>
</xsl:choose>
</xsl:map-entry>
</xsl:for-each-group>
</xsl:map>
</xsl:template>
<xsl:template match="*[not(*)]">
<xsl:sequence select="map { 'id' : data(#id) }"/>
</xsl:template>
</xsl:stylesheet>
https://xsltfiddle.liberty-development.net/6r5Gh3i
And here one of the second:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
exclude-result-prefixes="#all"
xmlns="http://www.w3.org/2005/xpath-functions"
expand-text="yes"
version="3.0">
<xsl:mode on-no-match="shallow-skip"/>
<xsl:strip-space elements="*"/>
<xsl:output method="text"/>
<xsl:variable name="json-xml">
<xsl:apply-templates/>
</xsl:variable>
<xsl:template match="/">
<xsl:sequence select="xml-to-json($json-xml, map { 'indent' : true() })"/>
</xsl:template>
<xsl:template match="*[*]">
<map>
<xsl:for-each-group select="foo" group-by="string(#group)">
<xsl:choose>
<xsl:when test="not(tail(current-group()))">
<map key="{current-grouping-key()}">
<string key="id">{#id}</string>
</map>
</xsl:when>
<xsl:otherwise>
<array key="{current-grouping-key()}">
<xsl:apply-templates select="current-group()"/>
</array>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</map>
</xsl:template>
<xsl:template match="*[not(*)]">
<map>
<string key="id">{#id}</string>
</map>
</xsl:template>
</xsl:stylesheet>
https://xsltfiddle.liberty-development.net/6r5Gh3i/5
XSLT 3 is available using Saxon 9.8 or 9.9 HE for the Java platform on Sourceforge and Maven https://mvnrepository.com/artifact/net.sf.saxon/Saxon-HE/9.9.1-2 and for the .NET platform on NuGet https://www.nuget.org/packages/Saxon-HE/.

Formatting XSLT document to display multiple nicknames on a single line

My xml documents contain a list of people, and these people can have 0 or more nicknames. I am having trouble trying to display all the nicknames properly in my xslt document.
I can have all the nicknames listed by using:
<xsl:for-each select="name/nickname">
Nickname: <xsl:value-of select="." />
</xsl:for-each>
The output of this is something like:
Nickname: nickname1
Nickname: nickname2
Which is a problem as I would like to get an output without Nickname: being listed so many times, i.e.
Nickname: nickname1, nickname2.
What I currently have is:
<p>
Nickname:
<xsl:for-each select="name/nickname">
<xsl:value-of select="." />,
</xsl:for-each>
</p>
Problems with this are:
Nickname will always be printed at least once even if a nickname doesn't exist.
There will always be a left over comma (,).
I am hoping there are suggestions to get around these two issues, I tried to use != "" but I'm not sure if this is allowed if an person doesn't contain a nickname.
Thanks :)
This transformation:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:template match="name[nickname]">
<xsl:text>
Nicknames: </xsl:text>
<xsl:apply-templates select="nickname"/>
</xsl:template>
<xsl:template match="nickname">
<xsl:if test="not(position() = 1)">
<xsl:text>, </xsl:text>
</xsl:if>
<xsl:apply-templates/>
</xsl:template>
</xsl:stylesheet>
when applied on this XML document:
<t>
<person>
<name trueName="John">
<nickname>X1</nickname>
<nickname>X2</nickname>
<nickname>X3</nickname>
</name>
</person>
<person>
<name trueName="Peter">
<nickname>Y1</nickname>
<nickname>Y2</nickname>
<nickname>Y3</nickname>
</name>
</person>
</t>
produces the wanted, correct result:
Nicknames: X1, X2, X3
Nicknames: Y1, Y2, Y3
Or,
<xsl:for-each select="name/nickname">
<xsl:if test="position() = 1">Nickname: </xsl:if>
<xsl:value-of select="." />
<xsl:if test="not(position()=last())">, </xsl:if>
</xsl:for-each>
Something like that (untested):
<xsl:when test="name/nickname">
Nickname:
<xsl:for-each select="name/nickname">
<xsl:value-of select="." />
<xsl:if test="count(following-sibling::nickname)">,</xsl:if>
</xsl:for-each>
</xsl:when>
Input :
<?xml version="1.0" encoding="UTF-8"?>
<test>
<nickname>1</nickname>
<nickname>2</nickname>
<nickname>3</nickname>
</test>
Transform :
<xsl:template match='/'>
<xsl:if test='count(//nickname) > 0'>
<result>
<xsl:for-each select='//nickname'>
<xsl:choose>
<xsl:when test='position() = 1'>
Nickname : <xsl:value-of select="."/><xsl:if test="not(position() = last())">,</xsl:if>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="."/>
<xsl:if test="not(position() = last())">,</xsl:if>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</result>
</xsl:if>
</xsl:template>
Output :
<?xml version="1.0" encoding="UTF-8"?>
<result>Nickname : 1,2,3</result>
Or in XSLT 2.0:
Input :
<?xml version="1.0" encoding="UTF-8"?>
<test>
<nickname>1</nickname>
<nickname>2</nickname>
<nickname>3</nickname>
</test>
Transform :
<xsl:template match='/'>
Nickname: <xsl:value-of select="/test/nickname" separator=", "/>
</xsl:template>