Home > Software engineering >  XSLT xml-to-json skips root node how to copy attribute as child node
XSLT xml-to-json skips root node how to copy attribute as child node

Time:09-10

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

  • Related