View Old View New View Both View Only Previous Next

This draft contains only sections that have differences from the version that it modified.

W3C

XSL Transformations (XSLT) Version 4.0

W3C Editor's Draft 23 February 2026

This version:
https://qt4cg.org/specifications/xslt-40/
Latest version:
https://qt4cg.org/specifications/xslt-40/
Most recent Recommendation of XSL Transformations (XSLT):
https://www.w3.org/TR/xslt-30/
Editor:
Michael Kay, Saxonica <http://www.saxonica.com/>

The following associated resources are available: Specification in XML format, XSD 1.1 Schema for XSLT 4.0 Stylesheets (non-normative), Relax-NG Schema for XSLT 4.0 Stylesheets (non-normative), Stylesheet for XML-to-JSON conversion (non-normative)


Abstract

This specification defines the syntax and semantics of XSLT 4.0, a language designed primarily for transforming XML documents into other XML documents.

XSLT 4.0 is a revised version of the XSLT 3.0 Recommendation [XSLT 3.0] published on 8 June 2017. Changes are presented in 1.2 What’s New in XSLT 4.0?.

XSLT 4.0 is designed to be used in conjunction with XPath 4.0, which is defined in [XPath 4.0]. XSLT shares the same data model as XPath 4.0, which is defined in [XDM 3.0], and it uses the library of functions and operators defined in [Functions and Operators 4.0]. XPath 4.0 and the underlying function library introduce a number of enhancements, for example the availability of union and record types.

This document contains hyperlinks to specific sections or definitions within other documents in this family of specifications. These links are indicated visually by a superscript identifying the target specification: for example XP for XPath 4.0, DM for the XDM data model version 4.0, FO for Functions and Operators version 4.0.

Status of this Document

This section describes the status of this document at the time of its publication. Other documents may supersede this document. A list of current W3C publications and the latest revision of this technical report can be found in the W3C technical reports index at https://www.w3.org/TR/.

This document has no official standing. It is produced by the editor as a proposal for community review. Insofar as it copies large amounts of text from the W3C XSLT 3.0 Recommendation, W3C copyright and similar provisions apply.

Dedication

The publications of this community group are dedicated to our co-chair, Michael Sperberg-McQueen (1954–2024).


5 Features of the XSLT Language

5.5 Named Types

XSLT 4.0 introduces two new and closely-related constructs allowing item types to be given names, and to be referred to by name anywhere that item types are used, for example in function declarations and variable declarations.

The xsl:item-type declaration allows any item type to be given a name. It is particularly useful to avoid repetitive use of the same choice types, enumeration types, or function types, and means that if the definition changes, the change only needs to be made in one place.

The xsl:record-type declaration takes this a step further. Like xsl:item-type, it allows a name to be given to any record type. In addition, though, it offers two further capabilities:

  • Named record types can be recursive, allowing definitions of recursive data structures such as lists and trees.

  • Declaring a named record type automatically establishes a constructor function for records of that type; the constructor function has the same name as the record type itself.

5.5.2 Named Record Types

Changes in 4.0  

  1. Named record types are introduced.   [Issue 1485  16 January 2025]

<!-- Category: declaration -->
<xsl:record-type
  name = eqname
  extensible? = boolean〔'no'〕
  visibility? = "private" | "public"〔'private'〕 >
  <!-- Content: (xsl:field*) -->
</xsl:record-type>

<xsl:field
  name = NCName
  as? = sequence-type〔'item()*'〕
  required? = boolean〔'yes'〕
  default? = expression />

An xsl:record-type declaration associates a name with a record type, and allows the record type to be referenced by name throughout the stylesheet package.

The following example declares a named record type for complex numbers, and uses it in a variable declaration and a function declaration.

<xsl:record-type name="cx:complex">
    <xsl:field name="r" as="xs:double"/>
    <xsl:field name="i" as="xs:double"/>
</xsl:record-type>
   
<xsl:variable name="i" as="cx:complex" select="cx:complex(0, 1)"/>
   
<xsl:function name="cx:add" as="cx:complex">
  <xsl:param name="x" as="cx:complex"/>
  <xsl:param name="y" as="cx:complex"/>
  <xsl:sequence select="cx:complex($x?r + $y?r, $x?i + $y?i)"/>
</xsl:function>

Note how the record type declaration has implicitly declared a constructor function cx:complex that can be used to create instances of the item type.

[ERR XTSE4050] It is a static error if the names of the fields in an xsl:record-type declaration are not distinct.

[ERR XTSE4051] It is a static error if an xsl:field element has a default attribute unless it specifies required="no".

An xsl:record-type declaration has two effects:

  • In the same way as xsl:item-type, it defines a named item type in the static context, allowing the record type to be referenced by name anywhere an ItemType can appear, for example in the declarations of functions and variables. Unlike types declared in xsl:item-type, however, named record types can be recursive.

  • An xsl:record-type declaration also implicitly defines a constructor function, of the same name, and adds this to the set of statically known function definitionsXP in the static context.

Because of its dual role, the name of an xsl:record-type declaration must be valid both as an item type name and as a function name. This means the name must be distinct from other type names and function names in the static context. It also means that the name must be in a namespace.

Considered as an item type, the xsl:record-type declaration is equivalent to an xsl:item-type declaration formed by the following template rule, evaluated in the context of a namespace alias <xsl:namespace-alias stylesheet-prefix="t" result-prefix="xsl"/>:

<xsl:template match="xsl:record-type" expand-text="yes">
   <t:item-type name="{@name}"
                visibility="{@visibility otherwise 'private'}">
     <xsl:attribute name="as">
        <xsl:text>record(</xsl:text>
        <xsl:for-each select="xsl:field" separator=", ">
           <xsl:variable name="optional"
                         as="xs:boolean"
                         select="normalize-space(@required) = ('no','false','0')"/>
           {@name}{'?'[$optional]} as {@as otherwise 'item()*'}
        </xsl:for-each>
        <xsl:if test="normalize-space(@extensible) = ('yes','true','1')">
           <xsl:text>, *</xsl:text>
        </xsl:if>
        <xsl:text>)</xsl:text>
      </xsl:attribute>  
   </t:item-type>
</xsl:template>

This generated xsl:item-type declaration must meet all the constraints placed on user-written xsl:item-type declarations, including the rule requiring names to be unique, but not including the rule disallowing recursive references.

For example, the declaration:

<xsl:record-type name="cx:complex" extensible="yes">
    <xsl:field name="r" as="xs:double"/>
    <xsl:field name="i" as="xs:double" required="no" default="0"/>
</xsl:record-type>

produces the equivalent item type declaration:

<xsl:item-type name="cx:complex"
       as="record(r as xs:double, i? as xs:double, *)"/>

Considered as a function declaration, an xsl:record-type declaration is equivalent to an xsl:function declaration formed by the following template rule, evaluated in the context of a namespace alias <xsl:namespace-alias stylesheet-prefix="t" result-prefix="xsl"/>:

<xsl:template match="xsl:record-type">
    <t:function name="{@name}"
                as="{@name}"
                visibility="{(@visibility otherwise 'private')}">
                
      <!-- if the record type is extensible, choose a name for the options parameter -->
      <xsl:variable name="options-param" as="xs:string?">
         <xsl:if test="normalize-space(@extensible) = ('yes', 'true', 'a')"
                 then="('options', (1 to count(xsl:field)) ! `options{.}`)
                         [not(. = current()/xsl:field/@name)][1]"/>
      </xsl:variable>                   
      
      <!-- declare the parameters -->
      <xsl:for-each select="xsl:field">
         <t:param name="{@name}"
                  required="{@required otherwise 'yes'}">
            <xsl:attribute name="as">
               <xsl:variable name="as" 
                             select="normalize-space(@as otherwise 'item()*')"/>
               <xsl:variable name="optional" 
                             select="normalize-space(@required) = ('no', 'false', '0')"/>
               <xsl:choose>
                  <xsl:when test="not($optional)" select="$as"/>
                  <!-- for optional fields, amend the required type to allow () -->
                  <xsl:when test="matches($as, '[?*]$')" select="$as"/>
                  <xsl:when test="matches($as, '\+$')" select="replace($as, '\+$', '*')"/>
                  <xsl:otherwise select="$as || '?'"/>
               </xsl:choose>
            </xsl:attribute>
            <xsl:if test="@default">
               <xsl:attribute name="select" select="@default"/>
            </xsl:if>   
         </t:param>
      </xsl:for-each>
      
      <!-- for an extensible record type, declare the options parameter -->
      <xsl:if test="exists($options-param)">
         <t:param name="{$options-param}" as="map(*)" required="no" select="{}"/>
      </xsl:if>           
      
      <!-- function body: construct a map -->
      <t:map on-duplicatesduplicates="fn($first, $second){{$first}}">
         <xsl:for-each select="xsl:field">
            <xsl:variable name="optional" 
                          select="normalize-space(@required) = ('no', 'false', '0')"/>
            <xsl:choose>
              <xsl:when test="$optional and not(@default)">
                 <!-- omit map entries for optional fields if no value is supplied -->
                 <t:if test="exists(${@name})">
                    <t:map-entry key="'{@name}'" select="${@name}"/>
                 </t:if>
              </xsl:when>
              <xsl:otherwise>
                <t:map-entry key="'{@name}'" select="${@name}"/>
              </xsl:otherwise>  
            </xsl:choose>  
         </xsl:for-each>
        
         <!-- if the record type is extensible, 
              add entries from the options parameter -->
              
         <xsl:if test="exists($options-param)">
            <t:sequence select="${$options-param}"/>
         </xsl:if>   

      </t:map>
    </t:function>          
</xsl:template>

For example, the declaration:

<xsl:record-type name="cx:complex">
    <xsl:field name="r" as="xs:double"/>
    <xsl:field name="i" as="xs:double" required="no" default="0"/>
</xsl:record-type>

produces the equivalent function declaration:

<xsl:function name="cx:complex" as="cx:complex" visibility="private">
    <xsl:param name="r" as="xs:double"/>
    <xsl:param name="i" as="xs:double" required="no" select="0"/>
    <xsl:map>
       <xsl:map-entry key="'r'" select="$r"/>
       <xsl:map-entry key="'i'" select="$i"/>
    </xsl:map>
</xsl:function>

No entry is generated in the constructed map for a field that is declared as optional with no default. So the declaration:

<xsl:record-type name="cx:complex" visibility="public">
    <xsl:field name="r" as="xs:double"/>
    <xsl:field name="i" as="xs:double" required="no"/>
</xsl:record-type>

produces the equivalent function declaration:

<xsl:function name="cx:complex" as="cx:complex" visibility="public">
    <xsl:param name="r" as="xs:double"/>
    <xsl:param name="i" as="xs:double?" required="no"/>
    <xsl:map>
       <xsl:map-entry key="'r'" select="$r"/>
       <xsl:if test="exists($i)">
          <xsl:map-entry key="'i'" select="$i"/>
       </xsl:if>   
    </xsl:map>
</xsl:function>

If the record type is extensible, the generated function includes an optional options parameter So the declaration:

<xsl:record-type name="cx:complex" extensible="yes">
    <xsl:field name="r" as="xs:double"/>
    <xsl:field name="i" as="xs:double"/>
</xsl:record-type>

produces the equivalent function declaration:

<xsl:function name="cx:complex" as="cx:complex" visibility="private">
    <xsl:param name="r" as="xs:double"/>
    <xsl:param name="i" as="xs:double"/>
    <xsl:param name="options" as="map(*)" required="no" default="{}"/>
    <xsl:map on-duplicatesduplicates="fn($first, $second){$first}">
       <xsl:map-entry key="'r'" select="$r"/>
       <xsl:map-entry key="'i'" select="$i"/>
       <xsl:sequence select="$options"/>   
    </xsl:map>
</xsl:function>

This generated xsl:function declaration must meet all the constraints placed on user-written xsl:function declarations, including the rule requiring the combination of name and arity to be unique.

The generated xsl:function declaration has the import precedence associated with the stylesheet module in which the xsl:record-type declaration appears, and it may be overridden by another xsl:function declaration with higher import precedence. If the visibility is public then it can also be overridden using xsl:override in another package.

The scope of a named record type is the package in which it is declared. If it is declared with visibility="public" then it also becomes available for use in using packages.

The name of the record type is the expanded name formed by resolving the name attribute. Because function names are always in a namespace, the name must be prefixed.

If two xsl:record-type declarations in a package have the same name, then the one with higher import precedence is used.

[ERR XTSE4030] It is a static error if a package contains two xsl:record-type declarations having the same import precedence, unless there is another definition of the same record type with higher import precedence.

A record type declaration may refer directly or indirectly to itself if it satisfies the conditions defined in Section 3.2.8.3.1 Recursive Record TypesXP. This allows types to be declared that match recursive data structures such as linked lists and trees.

[ERR XTSE4035] It is a static error for an item type named N to contain in its as attribute a reference to N, or to an item type that references N directly or indirectly, unless it satisfies the conditions defined in Section 3.2.8.3.1 Recursive Record TypesXP.

A named record type with visibility private may be used in the definition of a component (such as a variable, a function, a template, or another named record type) that is itself public. Another package may reference such a component even though it cannot reference the types used in its definition. The fact that the record type is private, however, means that it is impossible to override a variable or function that references the record type.

Example: Defining a Binary Tree

This example illustrates the definition of a recursive record type. The record type represents a node of a binary tree containing a payload value in each node, together with optional references to left and right subtrees. As well as the data fields, the record type defines a depth function that returns the maximum depth of the tree, by making recursive calls on its subtrees.

<xsl:record-type name="my:binary-tree">
   <xsl:field name="left" as="my:binary-tree?"/>
   <xsl:field name="payload" as="item()*"/>
   <xsl:field name="right" as="my:binary-tree?"/>
   <xsl:field name="depth" as="fn(my:binary-tree) as xs:integer"
      default="fn() {1 + max((?left =?> depth(), ?right =?> depth())) otherwise 0)}"/>
</xsl:record-type>

The =?> operator is described in [TITLE OF XP40 SPEC, TITLE OF lookup-arrow-expression SECTION]XP40. Its effect is to make a dynamic call on a function item that is present as an entry in a map, passing that map implicitly as the first argument. It thus mimics method invocation in object-oriented languages, though there is no inheritance or encapsulation.

The following code builds a simple tree and calculates its depth:

let $tree := my:binary-tree(
               my:binary-tree((), 17, ()),
               18,
               my:binary-tree((), 19, 
                 my:binary-tree((), 20, ())))
return $tree =?> depth()

Returning the result 3.

6 Template Rules

Template rules define the processing that can be applied to items that match a particular pattern.

6.8 Built-in Template Rules

Changes in 4.0  

  1. To allow recursive-descent transformation on a tree of maps and arrays, a new set of built-in templates rules shallow-copy-all is introduced.   [Issue 570 PR 718 26 September 2023]

When an item is selected by xsl:apply-templates and there is no user-specified template rule in the stylesheet that can be used to process that item, then a built-in template rule is evaluated instead.

The built-in template rules have lower import precedence than all other template rules. Thus, the stylesheet author can override a built-in template rule by including an explicit template rule.

There are seven sets of built-in template rules available. The set that is chosen is a property of the mode selected by the xsl:apply-templates instruction. This property is set using the on-no-match attribute of the xsl:mode declaration, which takes one of the values deep-copy, shallow-copy, shallow-copy-all,deep-skip, shallow-skip, text-only-copy, or fail, the default being text-only-copy. The effect of these seven sets of built-in template rules is explained in the following subsections.

6.8.4 Built-in Templates: Shallow Copy All

This processing mode is introduced in XSLT 4.0 as a variant of shallow-copy to enable recursive descent processing of trees involving maps and arrays, such as might result from parsing JSON input.

For all items other than maps and arrays, the effect of shallow-copy-all is exactly the same as shallow-copy.

For arrays, the processing is as follows. A new result array is created, and its content is populated by decomposing the input array to a sequence of value records using the function array:members. Each of these value records is processed by a call on xsl:apply-templates (using the current mode, and passing on the values of all template parameters); the result of the called template is expected to be a value record.

That is, the template rule is equivalent to the following, except that this does not show the propagation of template parameters:

<xsl:array use="?value">
  <xsl:apply-templates select="array:members(.)" mode="#current"/>
</xsl:array>

Note:

A value record is a singletonsingle-entry map: it has a single entrykey-value pair with the key "value", the corresponding value being a member of the original array. The default processing for a value record, unless specified otherwise, is to apply templates to the value, as indicated by the rules that follow.

For maps, the processing is as follows:

  • If the map contains two or more entries, then a new result map is created, and its content is populated by decomposing the input map using the function map:entries to produce a sequence of single-entry maps (each containing one key and one value), and then applying templates to this sequence, using the current mode, and passing on the values of all template parameters.

  • If the map contains a single entry { K : V0 }, then a new single entry map { K: V1 } is constructed in which V1 is the result of applying templates to V0 (using the current mode, and passing on the values of all template parameters).

    Note:

    This rule has the effect that if the input is a value record, the output will also be a value record.

  • If the map is empty, the result is an empty map.

In the first case, the template rule is equivalent to the following, except that this does not show the propagation of template parameters:

<xsl:map>
  <xsl:apply-templates select="map:entries(.)" mode="#current"/>
</xsl:map>

In the second case, the template rule is equivalent to the following, except that this does not show the propagation of template parameters:

<xsl:map-entry key="map:keys(.)">
  <xsl:apply-templates select="map:items(.)" mode="#current"/>
</xsl:map-entry>

The reason there is a special rule for maps with one entry is to ensure that the process terminates.

The overall effect is best understood with an example.

Example: Modified Identity Transformation of a JSON Document

The following stylesheet transforms a supplied JSON document by deleting all properties named "Note", appearing at any level:

<xsl:stylesheet version="3.0"
     xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
				  
<xsl:mode on-no-match="shallow-copy-all"/>

<xsl:template match="record(Note)">
  <!-- no action -->
</xsl:template>

</xsl:stylesheet>

Consider the following JSON input, converted to an array of maps by calling the function parse-json:

[
  { "Title": "Computer Architecture",
    "Authors": [ "Enid Blyton", { "Note": "possibly misattributed" } ],
    "Category": "Computers",
    "Price": 42.60
  },
  { "Title": "Steppenwolf",
    "Authors": [ "Hermann Hesse" ],
    "Category": "Fiction",
    "Price": 12.00,
    "Note": "out of print"
  },
  { "Title": "How to Explore Outer Space with Binoculars",
    "Authors": [ "Bruce Betts", "Erica Colon" ],
    "Category": "Science",
    "Price": 10.40
  }
]

The logic proceeds as follows:

  1. The outermost array is processed by applying templates to a sequence of value records, the first being in the form:

    { "value": map: { "Title": ..., "Author": ..., ... }

    The result of applying templates to these value records is expected to comprise a new sequence of value records, which is used to construct the final output array.

  2. Each of the value records is processed using the rule for singletonsingle-entry maps. This rule produces a new value record by applying templates to the value, that is, to a map of the form map: { "Title": ..., "Author": ..., ... } representing a book.

  3. Each of these books, being represented by a map with more than two entries, is processed by a template rule that splits the map into its multiple entries, each represented as a singleton map (a map with one key and one value). One of these singletonsingle-entry maps, for example, would be {"Title": "Steppenwolf"}.

  4. The default processing for a singletonsingle-entry map of the form { "Title": "Steppenwolf" } is to return the value unchanged. This is achieved by applying templates to the string "Steppenwolf"; the default template rule for strings returns the string unchanged.

  5. When a singletonsingle-entry map in the form { "Note": "out of print" } is encountered, no output is produced, meaning that entry in the parent map is effectively dropped. This is because there is an explicit template rule with match="record(Note)" that matches such singletonsingle-entry maps.

  6. When a singletonsingle-entry map in the form "Authors": [ "Bruce Betts", "Erica Colon" ] is encountered, a new singletonsingle-entry map is produced; it has the same key ("Authors"), and a value obtained by applying templates to the array [ "Bruce Betts", "Erica Colon" ]. The default processing for an array, in which none of the constituents are matched by explicit template rules, ends up delivering a copy of the array.

  7. When the singletonsingle-entry map "Authors": [ "Enid Blyton", { "Note": "possibly misattributed" } ] is encountered, the recursive processing results in templates being applied to the map { "Note": "possibly misattributed" }. This matches the template rule having match="record(Note)", which returns no output, so the entry is effectively deleted.

    Note:

    The map entry is deleted, but the map itself remains, so the value becomes "Authors": [ "Enid Blyton", map: {} ].

21 Maps

Maps are defined in the XDM Data Model: see Section 7.2 Map ItemsDM.

21.1 Map Instructions

Changes in 4.0  

  1. The xsl:map instruction allows a select attribute as an alternative to the contained sequence constructor.   [Issue 1632 ]

  2. The xsl:map-entry instruction, in common with other instructions, now raises error XTSE3185 (rather than XTSE3280) if both a select attribute and a sequence constructor are present.   [Issue 1632 ]

  3. Ordered maps are introduced.   [Issue 1651 PR 1703 14 January 2025]

Two instructions are added to XSLT to facilitate the construction of maps.

<!-- Category: instruction -->
<xsl:map
  select? = expression
  duplicates? = expression〔fn($a, $b) { error(xs:QName(err:XTDE3365)) }〕 >
  <!-- Content: sequence-constructor -->
</xsl:map>

The instruction xsl:map constructs and returns a new map.

The select attribute and the contained sequence constructor are mutually exclusive: if a select attribute is present, then the content must be empty except optionally for xsl:fallback instructions. [see ERR XTSE3185]

The result of evaluating the select expression or the contained sequence constructor is referred to as the input sequence.

The input sequence must be a sequence of maps: call this $maps.

In the absense of duplicate keys, the result of the instruction is then given by the XPath 3.1 expression:

map:merge($maps)

Note:

Informally: in the absence of duplicate keys the resulting map contains the union of the map entries from the supplied sequence of maps.

Note:

The order of entries in the returned map will reflect the order of items in the sequence that results from evaluation of the input sequence.

The handling of duplicate keys is described in 21.1.1 Handling of duplicate keys below.

There is no requirement that the supplied input maps should have the same or compatible types. The type of a map (for example map(xs:integer, xs:string)) is descriptive of the entries it currently contains, but is not a constraint on how the map may be combined with other maps.

Note:

A common coding pattern is to supply the input as a set of singletonsingle-entry maps, that is, maps containing a single entrykey-value pair. Moreover, it is often convenient to construct these using the xsl:map-entry instruction. However, it is not required that the input maps should be singletonssingle-entry maps, nor is it required that they should be constructed using this instruction.

[ERR XTTE3375] A type error occurs if the result of the input sequence is not an instance of the required type map(*)*.

Note:

In practice, the effect of this rule is that the result of the select expression or sequence constructor contained in the xsl:map instruction is severely constrained: it doesn’t make sense, for example, for it to contain instructions such as xsl:element that create new nodes. As with other type errors, processors are free to raise the error statically if they are able to determine that the sequence constructor would always fail when evaluated.

Note:

It is legitimate to construct a map using an instruction such as <xsl:map select="{'a':1, 'b':2}"/>. In this situation xsl:map has exactly the same effect as xsl:sequence, but users may feel that it improves readability.

<!-- Category: instruction -->
<xsl:map-entry
  key = expression
  select? = expression >
  <!-- Content: sequence-constructor -->
</xsl:map-entry>

The instruction xsl:map-entry constructs and returns a singleton map: that is, a map which contains one key and one value. Such a map is primarily used as a building block when constructing maps using the xsl:map instruction.

The select attribute and the contained sequence constructor are mutually exclusive: if a select attribute is present, then the content must be empty except optionally for xsl:fallback instructions. [see ERR XTSE3185]

The key of the entry in the new map is the value obtained by evaluating the expression in the key attribute, converted to the required type xs:anyAtomicType by applying the coercion rules. If the supplied key (after conversion) is of type xs:untypedAtomic, it is cast to xs:string.

The associated value is the value obtained by evaluating the expression in the select attribute, or the contained sequence constructor, with no conversion. If there is no select attribute and the sequence constructor is empty, the associated value is the empty sequence.

Example: Using XSLT instructions to create a fixed map

The following example binds a variable to a map whose content is statically known:

<xsl:variable name="week" as="map(xs:string, xs:string)">
  <xsl:map>
    <xsl:map-entry key="'Mo'" select="'Monday'"/>
    <xsl:map-entry key="'Tu'" select="'Tuesday'"/>
    <xsl:map-entry key="'We'" select="'Wednesday'"/>
    <xsl:map-entry key="'Th'" select="'Thursday'"/>
    <xsl:map-entry key="'Fr'" select="'Friday'"/>
    <xsl:map-entry key="'Sa'" select="'Saturday'"/>
    <xsl:map-entry key="'Su'" select="'Sunday'"/>
  </xsl:map>
</xsl:variable>

 

Example: Using XSLT instructions to create a computed map

The following example binds a variable to a map acting as an index into a source document:

<xsl:variable name="index" as="map(xs:string, element(employee))">
  <xsl:map>
    <xsl:for-each select="//employee">
      <xsl:map-entry key="@empNr" select="."/>
    </xsl:for-each>
  </xsl:map>
</xsl:variable>

 

Example: Modifying the keys in a map

The following example modifies a supplied map $input by changing all the keys to upper case. A dynamic error occurs if this results in duplicate keys:

<xsl:map>
  <xsl:for-each select="map:pairs($map)">
    <xsl:map-entry key="upper-case(?key)" select="?value"/>
  </xsl:for-each>
</xsl:map>

 

Example: Modifying the values in a map

The following example modifies a supplied map $input by wrapping each of the values in an array:

<xsl:map>
  <xsl:for-each select="map:pairs($map)">
    <xsl:map-entry key="?key">
       <xsl:array select="?value"/>
    </xsl:map-entry>
  </xsl:for-each>
</xsl:map>

This could also be written:

<xsl:map select="
  map:pairs($map) ! { ?key : array{ ?value } }"/>

21.1.1 Handling of duplicate keys

Changes in 4.0  

  1. A new attribute xsl:map/@duplicates is available, allowing control over how duplicate keys are handled by the xsl:map instruction.   [Issue 169  28 November 2023]

This section describes what happens when two or more maps in the input sequence of within an xsl:map instruction contain duplicate keys: that is, when one of these maps contains an entry with key K, and another contains an entry with key L, and fn:atomic-equal(K, L) returns true.

[ERR XTDE3365] In the absence of the duplicates attribute, a dynamic error occurs if the set of keys in the maps making up the input sequence contains duplicates.

The result of evaluating the duplicates attribute, if present, must be either one of the strings "use-first", "use-last", "use-any", "combine", or "reject", or a function with arity 2. These values correspond to the permitted values of the duplicates option of the map:of-pairsmap:merge function.

The result of the xsl:map instruction is defined by reference to the function map:of-pairsmap:merge. Specifically, if $maps is the input sequence to xsl:map, and $duplicates is the effective value of the duplicates attribute, then the result of the instruction is the result of the function call map:of-pairs(map:pairs($maps)map:merge($maps, { "duplicates": $duplicates }).

The following table shows some possible values of the duplicates attribute, and explains their effect:

FunctionAttributeEffect
duplicates="'use-first'"The first of the duplicate values is used.
duplicates="'use-last'"The last of the duplicate values is used.
duplicates="'combine'"The sequence concatenationXP of the duplicate values is used. This could also be expressed as on-duplicates="op(',')".
duplicates="fn($a, $b) { max(($a, $b)) }"The highest of the duplicate values is used.
duplicates="fn($a, $b) { min(($a, $b)) }"The lowest of the duplicate values is used.
duplicates="concat(?, ', ', ?) }"The comma-separated string concatenation of the duplicate values is used.
duplicates="op('+')"The sum of the duplicate values is used.
duplicates="fn($a, $b) { subsequence(($a, $b), 1, 4) }"The first four of the duplicates are retained; any further duplicates are discarded.
duplicates="fn($a, $b) { distinct-values(($a, $b)) }"When multiple entries have the same key, the corresponding values are retained only if they are distinct from other values having the same key.
Example: Combining Duplicates into an Array

This example takes as input an XML document such as:

<data>
   <event id="A23" value="12"/>
   <event id="A24" value="5"/>
   <event id="A25" value="9"/>
   <event id="A23" value="2"/>
 </data>

and constructs a map whose JSON representation is:

{ "A23": [ 12, 2 ], "A24": [ 5 ], "A23": [ 9 ] }

The logic is:

<xsl:template match="data">
   <xsl:map duplicates="fn($a, $b) { array:join(($a, $b)) }">
     <xsl:for-each select="event">
        <xsl:map-entry key="@id" select="[xs:integer(@value)]"/>
     </xsl:for-each>
   </xsl:map>
   </xsl:template>

Note:

Specifying the effect by reference to map:of-pairsmap:merge has the following consequences when duplicates are combined into a merged entry:

  • The position of the merged entry in the result corresponds to the position of the first of the duplicate keys in the input.

  • The key used for the merged entry in the result corresponds to one of the duplicate keys in the input: it is implementation-dependent which one is chosen. This is relevant when the duplicate keys differ in some way, for example when they have different type annotations, or when they are xs:dateTime values in different timezones.