#Data Types
XPath has a richer type system than JSON or most dynamically-typed languages. Understanding it prevents subtle bugs in comparisons, function calls, and XSLT template matching.
#Contents
#The Type Hierarchy
Every value in XPath is an item in a sequence. Items are either nodes (from the XML tree) or atomic values (strings, numbers, dates, etc.).
item()
├── node()
│ ├── document-node()
│ ├── element()
│ ├── attribute()
│ ├── text()
│ ├── comment()
│ ├── processing-instruction()
│ └── namespace-node()
├── xs:anyAtomicType
│ ├── xs:string
│ ├── xs:boolean
│ ├── xs:decimal
│ │ └── xs:integer
│ │ ├── xs:long
│ │ │ └── xs:int
│ │ │ └── xs:short
│ │ │ └── xs:byte
│ │ └── xs:nonNegativeInteger
│ │ └── xs:positiveInteger
│ ├── xs:double
│ ├── xs:float
│ ├── xs:date
│ ├── xs:dateTime
│ ├── xs:time
│ ├── xs:duration
│ │ ├── xs:dayTimeDuration
│ │ └── xs:yearMonthDuration
│ ├── xs:anyURI
│ ├── xs:QName
│ ├── xs:hexBinary
│ ├── xs:base64Binary
│ └── xs:untypedAtomic
├── function(*)
├── map(*)
└── array(*)
C# parallel: This is like the .NET type hierarchy (object → ValueType → int, string, DateTime, etc.), but with date/time types as first-class citizens rather than library types.
#Atomic Types in Practice
#Numeric Types
XPath has four numeric types with automatic promotion:
|
Type |
Range |
Precision |
C# Equivalent |
|---|---|---|---|
|
|
Arbitrary |
Exact |
|
|
|
Arbitrary |
Exact |
|
|
|
±3.4 × 10³⁸ |
~7 digits |
|
|
|
±1.7 × 10³⁰⁸ |
~15 digits |
|
Promotion rules (automatic, like C#):
integer → decimal → float → double5 + 3 => 8 (: integer + integer = integer :)
5 + 3.0 => 8.0 (: integer + decimal = decimal :)
5 + 3.0e0 => 8.0e0 (: integer + double = double :)
The xs:untypedAtomic gotcha: When you read a value from an XML element or attribute without a schema, it's xs:untypedAtomic — not xs:string. XPath automatically casts untypedAtomic to the required type in comparisons and arithmetic:
<price>39.99</price>
(: //price is xs:untypedAtomic, automatically cast to xs:double for comparison :)
//price > 30 => trueThis is convenient but means type errors surface at runtime, not compile time.
#String Type
Strings in XPath are sequences of Unicode characters. They're immutable, like C# strings.
"hello" (: string literal :)
'hello' (: also valid — single or double quotes :)
"" (: empty string :)
String vs untypedAtomic: An element's text content is xs:untypedAtomic, not xs:string. In most contexts this doesn't matter because they convert automatically. But xs:string values compare using collation, while xs:untypedAtomic values promote to the type of the other operand.
#Boolean Type
XPath booleans behave like C# booleans, but XPath has effective boolean value rules that automatically convert other types:
boolean("hello") => true (: non-empty string :)
boolean("") => false (: empty string :)
boolean(42) => true (: non-zero :)
boolean(0) => false (: zero :)#Date and Time Types
These are first-class types — not strings:
xs:date("2026-03-19") (: date :)
xs:time("14:30:00") (: time :)
xs:dateTime("2026-03-19T14:30:00") (: date + time :)
xs:duration("P1Y2M3D") (: duration :)
xs:dayTimeDuration("PT5H30M") (: days/hours/minutes/seconds :)
xs:yearMonthDuration("P1Y6M") (: years/months :)Date arithmetic works natively:
xs:date("2026-03-19") + xs:dayTimeDuration("P7D")
=> 2026-03-26 (: add 7 days :)
xs:date("2026-12-31") - xs:date("2026-01-01")
=> P365D (: difference as duration :)
xs:date("2026-03-19") > xs:date("2025-12-25")
=> true (: date comparison :)In C#, you'd need DateTime.AddDays(7), date2 - date1, etc. XPath uses standard operators.
#Type Casting
#Explicit Casting with Constructor Functions
xs:integer("42") => 42
xs:date("2026-03-19") => typed date
xs:double(42) => 42.0e0
xs:string(42) => "42"See Type Constructors for the full list.
#The cast as Expression
An alternative syntax for type casting:
"42" cast as xs:integer => 42
42 cast as xs:string => "42"
"true" cast as xs:boolean => true#The castable as Test
Tests whether a cast would succeed without actually performing it:
"42" castable as xs:integer => true
"abc" castable as xs:integer => false
"2026-03-19" castable as xs:date => true
C# equivalent: int.TryParse("42", out _)
Practical pattern — safe type conversion:
if ("42" castable as xs:integer)
then xs:integer("42")
else 0#The treat as Assertion
Asserts a type at compile time without converting. Raises an error if the type doesn't match at runtime:
$value treat as xs:integer (: assert $value is an integer :)
C# equivalent: (int)value — a cast that throws InvalidCastException on failure.
#The instance of Test
Tests whether a value is of a given type:
42 instance of xs:integer => true
42 instance of xs:string => false
"hello" instance of xs:string => true
(1, 2, 3) instance of xs:integer+ => true (: sequence of one or more integers :)
C# equivalent: value is int
#Sequence Types
Sequence types describe the structure of sequences. They're used in function signatures, variable declarations, and type tests.
#Occurrence Indicators
|
Indicator |
Meaning |
C# Equivalent |
|---|---|---|
|
(none) |
Exactly one |
|
|
|
Zero or one |
|
|
|
Zero or more |
|
|
|
One or more |
(no direct equivalent — non-empty enumerable) |
xs:integer (: exactly one integer :)
xs:integer? (: zero or one integer :)
xs:integer* (: zero or more integers :)
xs:integer+ (: one or more integers :)
item()* (: any sequence of any items :)
node() (: exactly one node :)
element(book) (: exactly one element named "book" :)#Function Types
Functions are first-class values with typed signatures:
function(xs:integer) as xs:integer (: function taking and returning an integer :)
function(xs:string, xs:string) as xs:boolean (: predicate on two strings :)
function(*) as item()* (: any function :)
C# equivalent: Func<int, int>, Func<string, string, bool>
#Map and Array Types
map(xs:string, xs:integer) (: map from strings to integers :)
map(*) (: any map :)
array(xs:string) (: array of strings :)
array(*) (: any array :)
C# equivalent: Dictionary<string, int>, List<string>
#Type Testing
#
instance of
Runtime type check:
let $value := //price/text()
return
if ($value instance of xs:decimal) then "decimal"
else if ($value instance of xs:string) then "string"
else "unknown"#
type() (XPath 4.0)
Returns the type name as a string:
type(42) => "xs:integer"
type("hello") => "xs:string"
type(xs:date("2026-03-19")) => "xs:date"
type(//price) => type of the price node's value
C# equivalent: value.GetType().Name
#Common Type Pitfalls
#1. Untyped Attribute Comparisons
(: @price is xs:untypedAtomic, not a number :)
//item[@price > 10] (: works — untypedAtomic auto-casts to double :)
//item[@price > "10"] (: WRONG — string comparison! "9" > "10" is true :)Rule: When comparing with a number literal, XPath casts the untyped value to a number. When comparing with a string literal, it does string comparison. Be explicit about which you want.
#2. Empty Sequence vs Empty String
() = "" (: false — empty sequence is not empty string :)
string(()) => "" (: but converting empty sequence gives empty string :)
//nonexistent = "" (: false — no nodes, not empty text :)
//nonexistent/text() = "" (: false for the same reason :)#3. Numeric String Comparison
"9" > "10" => true (: string comparison — "9" sorts after "1" :)
9 > 10 => false (: numeric comparison :)
xs:integer("9") > xs:integer("10") => false (: explicit numeric :)Rule: If you want numeric comparison, make sure at least one operand is a number.
#4. Date String Comparison
"2026-03-19" > "2025-12-25" => true (: works by accident — ISO format sorts correctly :)
xs:date("2026-03-19") > xs:date("2025-12-25") => true (: correct — typed date comparison :)ISO 8601 date strings happen to sort correctly as strings, but this is coincidental. Always use typed dates for reliable comparison, especially with non-ISO formats.
#5. Division Returns Decimal, Not Integer
10 div 3 => 3.333... (: not 3! :)
10 idiv 3 => 3 (: use idiv for integer division :)
C# difference: In C#, 10 / 3 returns 3 (integer division when both operands are integers). XPath's div always returns a decimal/double result.