Search code examples
xmlxsltxslt-1.0.net-4.8

XSLT producing a document with multiple default namespaces


I am trying to produce an XML document in the following form:

<?xml version="1.0" encoding="utf-8"?>
<Root xmlns="http://a.example.com">
  <Header xmlns="http://b.example.com">
    <Fr>
      <OrgId>
        <Id>hello</Id>
      </OrgId>
    </Fr>
    <To>
      <OrgId>
        <Id>goodbye</Id>
      </OrgId>
    </To>
  </Header>
  <Body xmlns="http://c.example.com">
    <Trades>
      <Volume>2</Volume>
      <Price>3</Price>
    </Trades>
  </Body>
</Root>

I have the following XSLT: Note this is XSLT 1.0 because I'm using .Net (.Net Framework)

<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" indent="yes"/>

  <xsl:template match="/">
    <Root xmlns="http://a.example.com">
      <Header xmlns="http://b.example.com">
        <Fr>
          <xsl:apply-templates select="/Trades">
            <xsl:with-param name="Id">hello</xsl:with-param>
          </xsl:apply-templates>
        </Fr>
        <To>
          <xsl:apply-templates select="/Trades">
            <xsl:with-param name="Id">goodbye</xsl:with-param>
          </xsl:apply-templates>
        </To>
      </Header>
      <Body xmlns="http://c.example.com">
        <Trades>
          <xsl:apply-templates select="/Trades/Trade" />
        </Trades>
      </Body>
    </Root>
  </xsl:template>

  <xsl:template match="/Trades">
    <xsl:param name="Id" />
    <OrgId>
      <Id>
        <xsl:value-of select="$Id"/>
      </Id>
    </OrgId>
  </xsl:template>

  <xsl:template match="Trade">
    <xsl:param name="Id" />
    <Volume>
      <xsl:value-of select="volume"/>
    </Volume>
    <Price>
      <xsl:value-of select="price"/>
    </Price>
  </xsl:template>

</xsl:stylesheet>

But this produces the following XML that does not validate against the 3rd party XSD

<?xml version="1.0" encoding="utf-8"?>
<Root xmlns="http://a.example.com">
  <Header xmlns="http://b.example.com">
    <Fr>
      <OrgId xmlns="">
        <Id>hello</Id>
      </OrgId>
    </Fr>
    <To>
      <OrgId xmlns="">
        <Id>goodbye</Id>
      </OrgId>
    </To>
  </Header>
  <Body xmlns="http://c.example.com">
    <Trades>
      <Volume xmlns="">2</Volume>
      <Price xmlns="">3</Price>
    </Trades>
  </Body>
</Root>

The problem is the xmlns="".

Why is the default namespace being explicitly reset?

The solution I've come up with which does validate against the XSD is as follows:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
>
  <xsl:output method="xml" indent="yes"/>

  <xsl:template match="/">
    <Root xmlns="http://a.example.com">
      <Header xmlns="http://b.example.com">
        <Fr>
          <xsl:apply-templates select="/Trades">
            <xsl:with-param name="Id">hellox</xsl:with-param>
            <xsl:with-param name="namespace">http://b.example.com</xsl:with-param>
          </xsl:apply-templates>
        </Fr>
        <To>
          <xsl:apply-templates select="/Trades">
            <xsl:with-param name="Id">goodbye</xsl:with-param>
            <xsl:with-param name="namespace">http://b.example.com</xsl:with-param>
          </xsl:apply-templates>
        </To>
      </Header>
      <Body xmlns="http://c.example.com">
        <Trades>
          <xsl:apply-templates select="/Trades/Trade">
            <xsl:with-param name="namespace">http://c.example.com</xsl:with-param>
          </xsl:apply-templates>
        </Trades>
      </Body>
    </Root>
  </xsl:template>

  <xsl:template match="/Trades">
    <xsl:param name="Id" />
    <xsl:param name="namespace" />
    <xsl:element name="b:OrgId" namespace="{$namespace}">
      <xsl:element name="b:Id" namespace="{$namespace}">
        <xsl:value-of select="$Id"/>
      </xsl:element>
    </xsl:element>
  </xsl:template>

  <xsl:template match="Trade">
    <xsl:param name="Id" />
    <xsl:param name="namespace" />
    <xsl:element name="c:Volume" namespace="{$namespace}">
      <xsl:value-of select="volume"/>
    </xsl:element>
    <xsl:element name="c:Price" namespace="{$namespace}">
      <xsl:value-of select="price"/>
      </xsl:element>
  </xsl:template>
</xsl:stylesheet>

This produces an XML document of this:

<?xml version="1.0" encoding="utf-8"?>
<Root xmlns="http://a.example.com">
  <Header xmlns="http://b.example.com">
    <Fr>
      <b:OrgId xmlns:b="http://b.example.com">
        <b:Id>hellox</b:Id>
      </b:OrgId>
    </Fr>
    <To>
      <b:OrgId xmlns:b="http://b.example.com">
        <b:Id>goodbye</b:Id>
      </b:OrgId>
    </To>
  </Header>
  <Body xmlns="http://c.example.com">
    <Trades>
      <c:Volume xmlns:c="http://c.example.com">2</c:Volume>
      <c:Price xmlns:c="http://c.example.com">3</c:Price>
    </Trades>
  </Body>
</Root>

This is acceptable as it validates against the XSD, but it feels unnecessarily complicated having to pass the namespace to the template and all the child elements in the templates need to have prefixed namespaces.

EDIT Forgot to add the input XML

<?xml version="1.0" encoding="utf-8" ?>
<Trades>
  <Trade>
    <id>
      a
    </id>
    <volume>2</volume>
    <price>3</price>
  </Trade>
</Trades>

Solution

  • It seems the point you are having problems with is that a literal result element inherits the namespaces from its ancestors in the stylesheet tree.

    If you want to have separate templates to deal with different branches of the input tree, you will have to re-declare the default namespace in each template. (Technically you could declare one of them as the "main" default namespace at the top level of the stylesheet, and then skip one of the declarations at the template level. But that would just add more confusion, IMHO).

    <xsl:stylesheet version="1.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" indent="yes"/>
    
    <xsl:template match="/">
        <Root xmlns="http://a.example.com">
            <Header xmlns="http://b.example.com">
                <Fr>
                    <xsl:apply-templates select="/Trades">
                        <xsl:with-param name="Id">hello</xsl:with-param>
                    </xsl:apply-templates>
                </Fr>
                <To>
                    <xsl:apply-templates select="/Trades">
                        <xsl:with-param name="Id">goodbye</xsl:with-param>
                    </xsl:apply-templates>
                </To>
            </Header>
            <Body xmlns="http://c.example.com">
                <Trades>
                    <xsl:apply-templates select="/Trades/Trade" />
                </Trades>
            </Body>
        </Root>
    </xsl:template>
    
    <xsl:template match="/Trades" xmlns="http://b.example.com">
        <xsl:param name="Id" />
        <OrgId >
            <Id>
                <xsl:value-of select="$Id"/>
            </Id>
        </OrgId>
    </xsl:template>
    
    <xsl:template match="Trade" xmlns="http://c.example.com">
        <xsl:param name="Id" />
        <Volume >
            <xsl:value-of select="volume"/>
        </Volume>
        <Price>
            <xsl:value-of select="price"/>
        </Price>
    </xsl:template>
    
    </xsl:stylesheet>
    

    You could probably simplify this by using xsl:for-each instead of passing the process to another template.