How to Join Values Using XPath concat()
Learn how XPath concat() joins strings, adds separators, handles nodes, and compares with string-join() across XPath versions.

concat() joins values by appending them in the order you provide. It does not add spaces, commas, or any other separator automatically. Put the separator in the argument list yourself:
concat('Ada', ' ', 'Lovelace')
The result is Ada Lovelace. Without the literal space, concat('Ada', 'Lovelace') returns AdaLovelace.
This guide explains the XPath 1.0 function, the XPath 2.0+ alternatives, node and empty-value behavior, predicates, XSLT examples, and practical troubleshooting.
What concat() does
XPath 1.0 defines the function as concat(string, string, string*): it requires at least two arguments and accepts additional arguments. The W3C specification describes it as returning the concatenation of its arguments. See the XPath 1.0 Recommendation.

concat('one', 'two', 'three')
Result:
onetwothree
Every character in the output comes from one of the arguments. A separator is simply another argument:
concat('North', ', ', 'South')
North, South
You can use literal strings, attributes, child elements, and expressions that evaluate to strings.
Basic patterns
| Goal | XPath | Result |
|---|---|---|
| Join two values | concat('A', 'B') |
AB |
| Add a space | concat('A', ' ', 'B') |
A B |
| Add punctuation | concat('A', ', ', 'B') |
A, B |
| Build a URL path | concat('/users/', @id) |
/users/42 |
| Add several pieces | concat(@scheme, '://', @host, @path) |
https://example.com/docs |
Joining element and attribute values
Given this XML:
<people>
<person given="Ada" family="Lovelace"/>
<person given="Grace" family="Hopper"/>
</people>
Select and combine one person’s attributes with:
/people/person[1]/concat(@given, ' ', @family)
That path is not valid XPath syntax in many processors because a function call cannot be appended directly after a location step. Select the node in a predicate or evaluate the function with node arguments from the current context:
concat(/people/person[1]/@given, ' ', /people/person[1]/@family)
Ada Lovelace
Inside a predicate, the context node makes the expression shorter:
//person[concat(@given, ' ', @family) = 'Ada Lovelace']
This selects the person element whose combined attributes equal the target string.
Element text values
For elements, pass the element expression itself or its string value:
concat(/people/person[1]/given, ' ', /people/person[1]/family)
XPath converts a node-set or node selection to a string when the function expects a string. In XPath 1.0, a node-set converted to a string contributes the string-value of its first node in document order. It does not produce one concatenated result for every matching node.
For example, with:
<person>
<given>Ada</given>
<given>Augusta</given>
<family>Lovelace</family>
</person>
concat(given, ' ', family) uses the first given element, yielding Ada Lovelace. If you need to process both given names, select each node and run the expression once per context node in your host language or XSLT template.
Separators are explicit
concat() never guesses your formatting. Include exactly the characters you want:
concat('2026', '-', '09', '-', '29')
2026-09-29
concat('Last name:', ' ', normalize-space(@family))
Use normalize-space() when source text may contain leading, trailing, or repeated whitespace. This is especially useful when joining scraped or human-authored XML content.
To conditionally include a separator, use an XPath conditional in XPath 2.0 or later:
concat(
normalize-space(given),
if (normalize-space(family) != '') then concat(' ', normalize-space(family)) else ''
)
XPath 1.0 has no if ... then ... else expression. In XSLT 1.0, use xsl:choose or build the output with separate xsl:value-of instructions.
concat() versus string-join()
Use concat() when you have a fixed number of pieces with potentially different formatting. Use string-join() when you have a sequence and want the same separator between adjacent items.
| Question | concat() |
string-join() |
|---|---|---|
| Input shape | Separate arguments | A sequence plus a separator |
| Separator | You place it wherever needed | Inserted between sequence items |
| XPath support | XPath 1.0 and newer | XPath 2.0 and newer |
| Typical use | Names, URLs, labels, mixed punctuation | Lists, paths, dynamically sized values |
string-join(('Ada', 'Augusta', 'Lovelace'), ' ')
Ada Augusta Lovelace
The XPath 3.1 Functions and Operators specification documents that string-join() places the separator between adjacent items and that concat() casts atomic arguments to strings. See the W3C XPath and XQuery Functions and Operators 3.1 Recommendation.
In XPath 3.1, an empty sequence supplied to concat() behaves as an empty string. In XPath 1.0, an empty node-set converts to an empty string. These rules make optional fields safe, but they can also create surprising double spaces or punctuation if you add separators unconditionally.
Complete XSLT example
The following stylesheet transforms XML people into a plain-text list. Save the input as people.xml and the stylesheet as people.xsl, then run them with an XSLT processor such as Saxon or another processor available in your environment.

<?xml version="1.0"?>
<people>
<person given="Ada" family="Lovelace"/>
<person given="Grace" family="Hopper"/>
</people>
<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:for-each select="people/person">
<xsl:value-of select="concat(@given, ' ', @family)"/>
<xsl:if test="position() != last()">
<xsl:text>
</xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
The for-each changes the context node for each person, so the relative attribute expressions refer to the current person. The explicit newline avoids adding an extra blank line at the end.
Running XPath from application code
Python with lxml
Install lxml with python -m pip install lxml, then run:
from lxml import etree
xml = etree.fromstring(b'''<people>
<person given="Ada" family="Lovelace"/>
<person given="Grace" family="Hopper"/>
</people>''')
names = xml.xpath("/people/person/concat(@given, ' ', @family)")
Some XPath 1.0 host APIs do not allow a function call in a location path like the last line. A portable approach evaluates the function relative to each selected node:
from lxml import etree
xml = etree.fromstring(b'''<people>
<person given="Ada" family="Lovelace"/>
<person given="Grace" family="Hopper"/>
</people>''')
names = [
person.xpath("concat(@given, ' ', @family)")
for person in xml.xpath('/people/person')
]
print(names)
# ['Ada Lovelace', 'Grace Hopper']
Node.js with the xpath package
Install a DOM parser and XPath implementation:
npm install @xmldom/xmldom xpath
const { DOMParser } = require('@xmldom/xmldom');
const xpath = require('xpath');
const doc = new DOMParser().parseFromString(`<people>
<person given="Ada" family="Lovelace"/>
<person given="Grace" family="Hopper"/>
</people>`);
const people = xpath.select('/people/person', doc);
const names = people.map(person =>
xpath.select('string(concat(@given, " ", @family))', person)
);
console.log(names); // [ 'Ada Lovelace', 'Grace Hopper' ]
Host APIs differ in whether an XPath expression returns a string, node, boolean, or number automatically. Use an explicit string(...) wrapper when you need a string result.
Handling optional, empty, and non-string values
Missing attributes normally contribute an empty string:
concat(@given, ' ', @family)
If only @given exists, the output ends with a space. Avoid that by conditionally adding the separator in XPath 2.0+:
concat(
normalize-space(@given),
if (normalize-space(@family) != '') then concat(' ', normalize-space(@family)) else ''
)
For numbers and booleans, XPath 2.0+ converts atomic values to strings. If formatting matters, use format-number() or an explicit conversion before concatenation. In XPath 1.0, numeric and boolean expressions are converted using XPath’s standard string conversion rules.
Do not expect concat() to iterate over a node-set. To join many nodes, use string-join() where supported, or iterate in XSLT or application code.
Common errors and fixes
| Error or symptom | Cause | Fix |
|---|---|---|
concat() returns words without spaces |
No separator argument was supplied. | Add a literal such as ' ', ', ', or '/'. |
| Only the first matching node appears | XPath 1.0 converts a node-set to the first node’s string value. | Iterate over nodes or use string-join() in XPath 2.0+. |
Unknown function string-join |
The processor supports XPath 1.0. | Use iteration, XSLT templates, or upgrade to an XPath 2.0+ processor. |
| Unexpected trailing or double spaces | An optional value is empty but the separator is unconditional. | Use normalize-space() and conditional logic. |
| Invalid expression near a path | A function was placed after a location step, or the host API expects a different return type. | Pass complete expressions to the evaluator, such as concat(/root/a, ' ', /root/b), and request a string result explicitly. |
| Quotes break the expression | The XPath string literal uses the same quote character as the host-language string. | Alternate single and double quotes, or use the host language’s escaping rules. |
| Whitespace from source XML remains | Element text contains indentation or line breaks. | Wrap values with normalize-space(). |
Performance, reliability, and maintainability
For a small fixed number of values, concat() is direct and easy to read. The main performance cost usually comes from repeatedly selecting nodes with broad expressions such as //item, parsing large documents, or evaluating the same expression inside nested loops. Select the narrowest context you can and evaluate the expression once per intended node.
When joining many values, string-join() communicates intent more clearly than constructing a long chain of concat() arguments. It also avoids manually placing a separator between every pair. Keep the XPath version in your deployment documentation because an expression valid in XPath 3.1 may fail in an XPath 1.0 engine.
For reliable output, decide how empty fields should behave before writing the expression. Test values that are missing, empty, whitespace-only, numeric, and repeated. If the output is used as an identifier or URL, normalize and validate each component before concatenating it.
Or skip the browser setup
If your goal is to capture the rendered result of an XPath-driven page, ScreenshotNeo provides a website screenshot API and MCP server. You can keep the XPath work in your application and request a clean image or PDF with one GET request. The complete API documentation is at screenshotneo.com/docs/.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and billing status. Its MCP server includes take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots each month without a card, and paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
FAQ
Does concat() add a separator automatically?
No. Add the separator as a literal argument, such as ' ' or ', '.
What is the minimum number of concat() arguments?
XPath 1.0 requires at least two arguments. Additional arguments are allowed.
Can concat() join an arbitrary list?
It is best for a fixed set of arguments. For a sequence of variable length, use string-join() in XPath 2.0 or later, or iterate in your host language.
What happens when an attribute is missing?
Its string value is empty. Add conditional logic if you do not want a leading, trailing, or doubled separator.
Why does my XPath engine reject string-join()?
Your processor probably supports XPath 1.0. Check the engine version and use iteration or an XSLT template when upgrading is not possible.


