#Copying and Identity Transform
Copying is one of the most common XSLT operations. Whether you are passing through most of a document unchanged, selectively modifying a few elements, or restructuring content, the copy instructions and the identity transform pattern are your primary tools.
#Contents
#xsl:copy
Creates a shallow copy of the current node — the node itself, but not its children, attributes, or namespaces (unless you explicitly process them).
#Basic Usage
<xsl:template match="product">
<xsl:copy>
<!-- Inside here, we are "inside" the copied <product> element -->
<!-- But it has no attributes or children yet — we must add them -->
<xsl:apply-templates select="@* | node()"/>
</xsl:copy>
</xsl:template>
xsl:copy copies:
-
Element nodes: Creates the element with the same name and namespace, but no attributes or children
-
Attribute nodes: Creates the attribute with the same name, namespace, and value
-
Text nodes: Creates a text node with the same content
-
Comment nodes: Creates a comment with the same content
-
Processing instruction nodes: Creates a PI with the same target and content
-
Document nodes: Creates a new document node
#copy-namespaces Attribute
By default, xsl:copy copies all in-scope namespace declarations. You can suppress this:
<xsl:copy copy-namespaces="no">
<xsl:apply-templates select="@* | node()"/>
</xsl:copy>This is useful when you want to clean up namespace declarations that are no longer needed.
#xsl:copy-of
Creates a deep copy of a node — the node, all its attributes, children, descendants, and namespace declarations. No further processing is done on the copied content.
<!-- Deep copy a single element with all its content -->
<xsl:copy-of select="product"/>
<!-- Deep copy all products -->
<xsl:copy-of select="//product"/>
<!-- Deep copy a variable's content -->
<xsl:copy-of select="$saved-fragment"/>#copy-of vs. copy
|
|
|
|
|---|---|---|
|
Depth |
Shallow (node only) |
Deep (entire subtree) |
|
Processing |
You control what goes inside |
None — exact replica |
|
Use case |
Modify-and-copy |
Pass-through |
|
Attributes |
Must be explicitly included |
Automatically included |
<!-- xsl:copy: shallow, you must fill it -->
<xsl:template match="product">
<xsl:copy>
<xsl:copy-of select="@*"/> <!-- copy all attributes -->
<xsl:apply-templates select="node()"/> <!-- process children -->
</xsl:copy>
</xsl:template>
<!-- xsl:copy-of: deep, exact replica -->
<xsl:copy-of select="product"/>#copy-of vs. sequence
xsl:copy-of creates a new, independent copy of the nodes. xsl:sequence returns a reference to the original nodes:
<!-- copy-of: new nodes, no connection to source -->
<xsl:variable name="snapshot">
<xsl:copy-of select="//product"/>
</xsl:variable>
<!-- Nodes in $snapshot have no parent in the source tree -->
<!-- sequence: references to original nodes -->
<xsl:variable name="refs" as="element(product)*">
<xsl:sequence select="//product"/>
</xsl:variable>
<!-- Nodes in $refs ARE the original nodes — ancestor::catalog still works -->C# parallel:
|
XSLT |
C# |
|---|---|
|
|
Deep clone: |
|
|
Reference: |
#The Identity Transform
The identity transform copies an entire document unchanged. It is the foundation for the "modify-and-copy" pattern — the most common XSLT design pattern.
#XSLT 1.0 Version (Verbose)
<!-- Copy everything by default -->
<xsl:template match="@* | node()">
<xsl:copy>
<xsl:apply-templates select="@* | node()"/>
</xsl:copy>
</xsl:template>This single template handles every node type:
-
@*matches any attribute —xsl:copycopies it -
node()matches elements, text, comments, and PIs —xsl:copycreates a shallow copy, thenxsl:apply-templatesrecursively processes children and attributes
The result: a bit-for-bit copy of the entire input document. On its own, this is useless. The power comes from adding override templates that handle specific nodes differently while everything else passes through unchanged.
#XSLT 3.0 Version (Concise)
XSLT 3.0 lets you declare the identity transform behavior on a mode, eliminating the need for the explicit template:
<xsl:mode on-no-match="shallow-copy"/>This single declaration says: "For any node that does not match an explicit template, do a shallow copy and process its children." It replaces the entire identity template.
Your stylesheet then only contains templates for the nodes you want to change:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0">
<xsl:mode on-no-match="shallow-copy"/>
<!-- Only this template is needed — everything else passes through -->
<xsl:template match="price">
<price currency="USD"><xsl:value-of select=". * 1.1"/></price>
</xsl:template>
</xsl:stylesheet>C# parallel: This is the visitor pattern. In C#:
class PriceAdjuster : XmlVisitor
{
// Override only what you need to change
public override XElement VisitElement(XElement element)
{
if (element.Name == "price")
return new XElement("price",
new XAttribute("currency", "USD"),
(decimal)element * 1.1m);
// Default: visit children (identity behavior)
return base.VisitElement(element);
}
}#on-no-match Behaviors
The on-no-match attribute on xsl:mode controls what happens to nodes that do not match any explicit template. XSLT 3.0 defines six behaviors:
|
Value |
Behavior |
Use Case |
|---|---|---|
|
|
Copy the node, process children |
Identity transform (most common) |
|
|
Copy the node and all descendants without processing |
Pass-through with no modification possible |
|
|
Output only text content (XSLT 1.0 built-in behavior) |
Default if |
|
|
Skip the node, but process its children |
Strip wrappers, keep content |
|
|
Skip the node and all descendants |
Suppress unmatched content entirely |
|
|
Raise an error |
Ensure every node type is explicitly handled |
#shallow-copy (Identity)
<xsl:mode on-no-match="shallow-copy"/>This is the default for modify-and-copy stylesheets. Every unmatched node is copied; only your explicit templates alter the output.
#deep-copy
<xsl:mode on-no-match="deep-copy"/>Like shallow-copy, but unmatched nodes are deep-copied without processing their children. This means templates cannot fire on descendants of unmatched nodes. Rarely used.
#text-only-copy
<xsl:mode on-no-match="text-only-copy"/>This is the XSLT 1.0 / 2.0 default behavior (the built-in template rules). Unmatched elements are skipped but their children are processed, so text content bubbles up. This is why an XSLT stylesheet with no templates outputs all the text from the document.
#shallow-skip
<xsl:mode on-no-match="shallow-skip"/>Unmatched nodes are dropped from the output, but their children are still processed. Use this when you want to strip all wrapper elements and only keep content that your templates explicitly handle.
<!-- Strip all formatting elements, keep only the text they wrap -->
<xsl:mode on-no-match="shallow-skip"/>
<xsl:template match="p | li | td">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<!-- Result: <b>, <i>, <span>, etc. are stripped; their text content remains -->#deep-skip
<xsl:mode on-no-match="deep-skip"/>Unmatched nodes and all their descendants are completely suppressed. Only nodes that match an explicit template appear in the output.
#fail
<xsl:mode on-no-match="fail"/>Raises an error if any node does not match a template. Use this during development to ensure complete coverage — every element type in your input must have a matching template.
#Practical Patterns
All of the following patterns assume you have the identity transform in place (either the XSLT 1.0 template or <xsl:mode on-no-match="shallow-copy"/>).
#Modifying Attribute Values
Change a specific attribute while preserving everything else:
<xsl:mode on-no-match="shallow-copy"/>
<!-- Update all prices by 10% -->
<xsl:template match="product/price/text()">
<xsl:value-of select=". * 1.1"/>
</xsl:template>Or modify an attribute:
<!-- Change status from "draft" to "published" -->
<xsl:template match="product/@status[. = 'draft']">
<xsl:attribute name="status">published</xsl:attribute>
</xsl:template>#Adding Attributes
<!-- Add a "processed" timestamp to every product -->
<xsl:template match="product">
<xsl:copy>
<xsl:apply-templates select="@*"/>
<xsl:attribute name="processed"><xsl:value-of select="current-dateTime()"/></xsl:attribute>
<xsl:apply-templates select="node()"/>
</xsl:copy>
</xsl:template>
Note: The order matters — attributes must be added before any child nodes. xsl:attribute after a child element is an error.
#Removing Attributes
<!-- Remove the "internal-notes" attribute from all elements -->
<xsl:template match="@internal-notes"/>
<!-- Remove all "debug-*" attributes -->
<xsl:template match="@*[starts-with(local-name(), 'debug-')]"/>An empty template matches the node and produces nothing — effectively deleting it from the output.
#Renaming Elements
<!-- Rename <item> to <product> -->
<xsl:template match="item">
<product>
<xsl:apply-templates select="@* | node()"/>
</product>
</xsl:template>
<!-- Rename <desc> to <description> -->
<xsl:template match="desc">
<description>
<xsl:apply-templates select="@* | node()"/>
</description>
</xsl:template>#Filtering Elements (Suppressing Nodes)
<!-- Remove all <internal-notes> elements -->
<xsl:template match="internal-notes"/>
<!-- Remove products that are discontinued -->
<xsl:template match="product[@status = 'discontinued']"/>
<!-- Remove empty elements -->
<xsl:template match="*[not(node()) and not(@*)]"/>#Adding New Child Elements
<!-- Add a calculated <total> element to each order-item -->
<xsl:template match="order-item">
<xsl:copy>
<xsl:apply-templates select="@* | node()"/>
<!-- Add new element after existing children -->
<total><xsl:value-of select="price * quantity"/></total>
</xsl:copy>
</xsl:template>#Wrapping Elements
<!-- Wrap each <product> in a <div> -->
<xsl:template match="product">
<div class="product-wrapper">
<xsl:copy>
<xsl:apply-templates select="@* | node()"/>
</xsl:copy>
</div>
</xsl:template>#Unwrapping Elements
<!-- Remove the <wrapper> element but keep its content -->
<xsl:template match="wrapper">
<xsl:apply-templates select="node()"/>
</xsl:template>#Restructuring (Moving Elements)
<!-- Move <price> from being a child of <details> to a direct child of <product> -->
<xsl:template match="product">
<xsl:copy>
<xsl:apply-templates select="@*"/>
<!-- Pull price out of details -->
<xsl:apply-templates select="details/price"/>
<xsl:apply-templates select="node()"/>
</xsl:copy>
</xsl:template>
<!-- Suppress price in its original location (it was already output above) -->
<xsl:template match="details/price"/>#Conditional Modification
<!-- Only modify products in the "electronics" category -->
<xsl:template match="product[@category = 'electronics']/price/text()">
<xsl:value-of select=". * 0.9"/> <!-- 10% discount on electronics -->
</xsl:template>
<!-- All other products pass through unchanged (identity transform handles them) -->#Complete Example: Document Migration
Here is a realistic stylesheet that migrates a product catalog from an old schema to a new one:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0">
<xsl:mode on-no-match="shallow-copy"/>
<!-- Rename root element -->
<xsl:template match="product-list">
<catalog version="2.0">
<xsl:apply-templates select="@* | node()"/>
</catalog>
</xsl:template>
<!-- Rename item to product, add required ID if missing -->
<xsl:template match="item">
<product>
<xsl:if test="not(@id)">
<xsl:attribute name="id" select="generate-id()"/>
</xsl:if>
<xsl:apply-templates select="@* | node()"/>
</product>
</xsl:template>
<!-- Rename desc to description -->
<xsl:template match="item/desc">
<description><xsl:apply-templates/></description>
</xsl:template>
<!-- Convert price from string to structured element -->
<xsl:template match="item/cost">
<price currency="{(@currency, 'USD')[1]}">
<xsl:value-of select="format-number(number(.), '#0.00')"/>
</price>
</xsl:template>
<!-- Remove deprecated elements -->
<xsl:template match="legacy-code | internal-ref"/>
<!-- Remove deprecated attributes -->
<xsl:template match="@old-system-id"/>
</xsl:stylesheet>C# parallel: The entire identity-transform-with-overrides pattern is the visitor pattern. In C#, you would write:
// C# visitor pattern equivalent
class SchemaMigrationVisitor : XmlVisitor
{
protected override XElement Visit(XElement element) => element.Name.LocalName switch
{
"product-list" => new XElement("catalog", new XAttribute("version", "2.0"),
element.Attributes().Select(Visit),
element.Elements().Select(Visit)),
"item" => new XElement("product", /* ... */),
"desc" => new XElement("description", element.Value),
"legacy-code" => null, // suppress
_ => base.Visit(element) // identity (pass through)
};
}Or using C# 9+ record expressions for immutable modification:
// Record-style immutable modification (conceptually similar)
var updated = original with { Price = original.Price * 1.1m };The XSLT approach is more concise for XML-to-XML transformations because the identity transform handles the boilerplate of copying every node you did not explicitly change.