I am trying to convert XML to json using XSLT 3.0, lowercasing all keys and moving the first attribute, if any, as a JSON child. So given following (dummy) input XML:
<FOO id="1">
<BAR xy="2">
<SPAM>N</SPAM>
</BAR>
</FOO>
I am expecting
{
"foo" : {
"id" : "1",
"bar" : {
"xy" : "2",
"spam" : "N"
}
}
}
Using this XSLT:
<?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="http://www.w3.org/2005/xpath-functions"
exclude-result-prefixes="#all"
expand-text="yes"
version="3.0">
<xsl:output method="xml" indent='true' omit-xml-declaration='yes'/>
<xsl:template match="dummy">
<xsl:variable name="json-xml">
<xsl:apply-templates/>
</xsl:variable>
<xsl:value-of select="xml-to-json($json-xml, map { 'indent' : true() })"/>
</xsl:template>
<!--no children-->
<xsl:template match="*[not(*)]">
<string key="{lower-case(local-name())}">{.}</string>
</xsl:template>
<xsl:template match="*[*]">
<xsl:param name="key" as="xs:boolean" select="true()"/>
<map>
<xsl:if test="$key">
<xsl:attribute name="key" select="lower-case(local-name())"/>
</xsl:if>
<xsl:for-each-group select="*" group-by="node-name()">
<xsl:choose>
<xsl:when test="current-group()[2]">
<array key="{lower-case(local-name())}">
<xsl:apply-templates select="current-group()">
<xsl:with-param name="key" select="false()"/>
</xsl:apply-templates>
</array>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="current-group()">
<xsl:with-param name="key" select="true()"/>
</xsl:apply-templates>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</map>
</xsl:template>
</xsl:stylesheet>
I get (note the dummy
pattern to skip json conversion),
<map xmlns="http://www.w3.org/2005/xpath-functions" key="foo">
<map key="bar">
<string key="spam">N</string>
</map>
</map>
Looks good to me but when I invoke the JSON conversion (by replacing dummy
with /
), I get:
{ "bar" :
{ "spam" : "N" } }
-> the foo
node is gone.
I haven't figured out how to "move" the first attribute (arbitrary, could have any name) as a child node - if someone knows, appreciate a little snippet.
Lastly, not a big-deal but I am lowercasing keys in each template. Is it possible to do the transformation at once, either before in the source XML, or after templating, in the Result XML (before jsonifying) ?
See - https://xsltfiddle.liberty-development.net/jyfAiDC/2
(thanks btw to @Martin Honnen for this very useful tool !!)
CodePudding user response:
You can use
<xsl:template match="/">
<xsl:variable name="json-xml">
<map>
<xsl:apply-templates/>
</map>
</xsl:variable>
<xsl:value-of select="xml-to-json($json-xml, map { 'indent' : true() })"/>
</xsl:template>
perhaps to get closer to what you might want. But I haven't understood what you want to do with attributes in the XML.
CodePudding user response:
The key
attribute only make sense for an element that represents an entry in a map, so there's no point including it in an element unless that element has a parent named map
. You want another layer of map in your structure.
CodePudding user response:
Thanks to Martin & Michael for the map
wrapper tip. Agreed though it is an unnecessary level in tree.
For rendering a XML attribute as a child node - assuming there is one only -,
I added the following after the first test
(conditional map attribute) in the template:
<xsl:if test="@*[1]">
<string key="{name(@*[1])}">{@*[1]}</string>
</xsl:if>
Lastly, for converting to lowercase all intermediary key
attributes in one-go instead of individually in multiple templates, it would require I think parsing the result tree before passing on to the xml-to-json
function.
Not worth it... but it would be a nice featured option in XSLT 4.0
(?) i.e. a new xml-to-json
option force-key-case
= lower/upper