ScreenshotNeo

BlogGuides

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.

By the ScreenshotNeo team29 September 20268 min read

How to Join Values Using XPath concat()

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.

XPath concat() combines fixed values; separators must be supplied as their own arguments.
XPath concat() combines fixed values; separators must be supplied as their own arguments.
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.

Use concat() for fixed pieces and string-join() for a sequence with a repeated delimiter.
Use concat() for fixed pieces and string-join() for a sequence with a repeated delimiter.
<?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.