1. Introduction
When you see XSLT for the first time, a natural thought comes up: “Wait… we already have XPath. Why another language, and one that even looks like XML?” That’s a normal reaction. XPath is about selecting data. XSLT is about assembling an output by rules. Think of it this way: XPath is “find the right ingredients,” and XSLT is “cook the dish and serve it nicely.”
More practically, XSLT shows up where you need to:
- transform one XML contract into another (for example, “partner’s legacy format → our format”);
- generate a text report from XML (almost like a template engine, but for XML);
- keep the transformation “on the integration side,” without rewriting code in Java/Kotlin.
And an important point for Kotlin/JVM: we don’t need to hunt for exotic libraries — the JVM has historically had standard APIs for XSLT, and Kotlin can call them just fine because it plays great with the Java ecosystem.
Mini-table: XPath vs XSLT
| Tool | Main idea | Result | Typical question |
|---|---|---|---|
| XPath | find/select | nodes or values | “Where in the document is user[@id='42']/name?” |
| XSLT | transform/assemble | another XML or text | “How do I turn <user> into <person> or into a report line?” |
2. The XSLT mental model: “input + rules → output”
Before the code, it helps to get your head into the right picture. XSLT is a set of rules (templates) that say: “if the current node is like this, then the output document should contain that.” It’s similar to when, but in the XML tree world: instead of values you have nodes, instead of branches you have templates, and instead of println you generate output.
From a “pure understanding” standpoint, three ideas are enough:
- XSLT is also an XML document that contains xsl:* elements.
- A template (xsl:template) selects a part of the input XML (via match="..."), and inside it describes what to write to the output.
- To extract data from the input XML, most often you use XPath expressions inside XSLT (select="...").
So XPath inside XSLT doesn’t disappear — it becomes an “embedded navigator” over the input document.
3. The smallest XSLT that actually makes sense
Let’s assemble a minimal but working XSLT. We’ll take an input XML and make the output text to make the result easy to see (like a regular println, just done through a transformation).
Input XML
<user id="42">
<name>Ann</name>
</user>
XSLT
We want to get the line: User: Ann.
For XSLT to be “proper,” it needs a root xsl:stylesheet element, the correct namespace, and at least one template.
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/user">
User: <xsl:value-of select="name"/>
</xsl:template>
</xsl:stylesheet>
Two things are important to notice here. First, match="/user" means “this template is applied to the root <user> element.” Second, select="name" is XPath relative to <user>, i.e., “take the child <name> element.”
4. A minimal JVM pipeline: TransformerFactory → Transformer → transform(...)
Now let’s move on to the “execution mechanics.” The JDK has the javax.xml.transform package, which can compile XSLT and apply it. Kotlin code looks almost like Java, just without extra ceremony.
The pipeline looks like this:
flowchart LR
A[XML Source] -->|transform| T[Transformer]
S[XSLT Source] -->|compile| F[TransformerFactory]
F --> T
T --> B[Result: text/xml]
We need three kinds of entities:
- Source for the input XML,
- Source for the XSLT,
- Result for the output.
Example: “XML string → text string”
Note: the example is short (and intentionally “straight to the point”) so you can see the pipeline itself.
import java.io.StringReader
import java.io.StringWriter
import javax.xml.transform.TransformerFactory
import javax.xml.transform.stream.StreamResult
import javax.xml.transform.stream.StreamSource
fun main() {
val xml = "<user id=\\"42\\"><name>Ann</name></user>"
val xslt = """<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/user">User: <xsl:value-of select="name"/></xsl:template>
</xsl:stylesheet>""".trimIndent()
val transformer = TransformerFactory.newInstance()
.newTransformer(StreamSource(StringReader(xslt)))
val out = StringWriter()
transformer.transform(StreamSource(StringReader(xml)), StreamResult(out))
println(out.toString().trim()) // User: Ann
}
Here StreamSource(StringReader(...)) is a way to “feed a string” as a source. StringWriter is our output sink (like “printing into a string”).
5. Practice: XSLT for reports and contract conversion
So the examples don’t feel like isolated islands, let’s continue the same line from the XML/DOM/XPath lectures: we have a small XML with data, and we want to:
- either turn it into a human-readable report (text),
- or turn it into another XML with a more convenient structure.
Let’s imagine that in our learning console app we store purchases (basically the simplest “expenses”) in XML:
<purchases>
<purchase category="food" amount="12.50">Milk</purchase>
<purchase category="food" amount="2.10">Bread</purchase>
<purchase category="fun" amount="15.00">Movie</purchase>
</purchases>
In previous lectures, we could parse this with DOM and compute totals. Now we’ll do a different trick: let XSLT assemble a text report itself.
XSLT “XML → text”: a simple list report
It’s important not to try to build a “report of everything in the world” in XSLT: this is an overview. It’s enough to learn basic things: iterate over elements and print fields.
XSLT for the report
This uses xsl:for-each — “iterate over a node set.” Inside, we take attributes via @category and @amount, and the purchase text via ..
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/purchases">
Purchases report:
<xsl:for-each select="purchase">
- <xsl:value-of select="."/> (<xsl:value-of select="@category"/>): $<xsl:value-of select="@amount"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Notice a “practical” detail: spaces and line breaks in XSLT with method="text" become part of the result. That’s not a bug, that’s a feature. But because of that, sometimes you need to do trim() on the output in Kotlin—exactly like we did trim() on textContent.
Running from Kotlin
import java.io.StringReader
import java.io.StringWriter
import javax.xml.transform.TransformerFactory
import javax.xml.transform.stream.StreamResult
import javax.xml.transform.stream.StreamSource
fun main() {
val xml = """<purchases>
<purchase category="food" amount="12.50">Milk</purchase>
<purchase category="food" amount="2.10">Bread</purchase>
</purchases>""".trimIndent()
val xslt = """<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/purchases">
Purchases report:
<xsl:for-each select="purchase">
- <xsl:value-of select="."/> (<xsl:value-of select="@category"/>): $<xsl:value-of select="@amount"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>""".trimIndent()
val t = TransformerFactory.newInstance().newTransformer(StreamSource(StringReader(xslt)))
val out = StringWriter()
t.transform(StreamSource(StringReader(xml)), StreamResult(out))
println(out.toString().trim())
// Purchases report:
// - Milk (food): $12.50
// - Bread (food): $2.10
}
Yes, it looks like “some weird old-school template engine.” But that’s exactly how XSLT often lives in integrations: a small template file and code that applies it.
XSLT “XML → XML”: changing the data structure for another contract
Now let’s do the second typical case: transform one XML into another. This is useful when:
- the input XML is awkward for XPath/DOM analysis;
- you want to normalize data into a unified format before processing.
For example, suppose the original purchases are stored as:
<purchase category="food" amount="12.50">Milk</purchase>
But we want to get:
<item>
<title>Milk</title>
<category>food</category>
<amount>12.50</amount>
</item>
XSLT that assembles a new XML
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/purchases">
<items>
<xsl:for-each select="purchase">
<item>
<title><xsl:value-of select="."/></title>
<category><xsl:value-of select="@category"/></category>
<amount><xsl:value-of select="@amount"/></amount>
</item>
</xsl:for-each>
</items>
</xsl:template>
</xsl:stylesheet>
Running from Kotlin
import java.io.StringReader
import java.io.StringWriter
import javax.xml.transform.TransformerFactory
import javax.xml.transform.stream.StreamResult
import javax.xml.transform.stream.StreamSource
fun main() {
val xml = """<purchases>
<purchase category="food" amount="12.50">Milk</purchase>
</purchases>""".trimIndent()
val xslt = """<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/purchases">
<items><xsl:for-each select="purchase">
<item>
<title><xsl:value-of select="."/></title>
<category><xsl:value-of select="@category"/></category>
<amount><xsl:value-of select="@amount"/></amount>
</item>
</xsl:for-each></items>
</xsl:template>
</xsl:stylesheet>""".trimIndent()
val out = StringWriter()
TransformerFactory.newInstance()
.newTransformer(StreamSource(StringReader(xslt)))
.transform(StreamSource(StringReader(xml)), StreamResult(out))
println(out.toString())
}
Now the important connection to the previous lecture: after the XSLT transformation, you can take the result (as an XML string), parse it into DOM again, and use XPath on the new structure. Sometimes this really simplifies logic if the original format is a bit “wonky.”
XSLT from files: what it looks like in real life
Up to this point, we kept XSLT in a string. That’s fine for a learning example, but in a real project XSLT almost always lives as a separate file next to the application (or comes in as a resource).
From the JVM API perspective, the run is the same; it’s just that StreamSource comes from a file.
Example: apply report.xslt to purchases.xml
import java.io.File
import javax.xml.transform.TransformerFactory
import javax.xml.transform.stream.StreamResult
import javax.xml.transform.stream.StreamSource
fun main() {
val xmlFile = File("purchases.xml")
val xsltFile = File("report.xslt")
val transformer = TransformerFactory.newInstance()
.newTransformer(StreamSource(xsltFile))
transformer.transform(StreamSource(xmlFile), StreamResult(System.out))
// The output will go straight to the console
}
StreamResult(System.out) is very handy for console tools: the XSLT result is printed immediately, and you don’t need to build a string.
6. Errors and diagnostics: what exactly “broke” — the XML or the XSLT
In practice, XSLT adds one more layer of possible problems. Previously, your input XML could “break” (won’t parse, not well‑formed). Now the XSLT can break too: for example, a wrong namespace, a syntax error, or referencing a node that doesn’t exist (sometimes this just yields an empty result, sometimes an error— it depends on the situation and the engine).
A good habit: in code, separate the stages.
- Stage 1: “compile” XSLT into a Transformer
- Stage 2: apply transform(...)
Mini example with try/catch and a clear message
import java.io.StringReader
import javax.xml.transform.TransformerFactory
import javax.xml.transform.stream.StreamSource
fun main() {
val brokenXslt = "<xsl:stylesheet></xsl:stylesheet>" // no xmlns:xsl
try {
TransformerFactory.newInstance()
.newTransformer(StreamSource(StringReader(brokenXslt)))
println("XSLT ok")
} catch (e: Exception) {
println("XSLT error: ${e.message}")
}
}
This example may look “too simple,” but it shows the key point: an XSLT error is not the same as an input XML error. And when you debug an integration, it’s important not to write a generic “something went wrong,” but at least to understand at which step it happened.
7. Typical mistakes when working with XSLT
Mistake #1: confusing XPath and XSLT and expecting evaluate(...) to “generate output.”
XPath by itself doesn’t “render” or “build” anything. It selects values or nodes. If you need to assemble a new XML or a text report, XPath is only part of the solution, and the output assembly must be described with XSLT templates.
Mistake #2: forgetting xmlns:xsl="http://www.w3.org/1999/XSL/Transform" and getting mysterious errors.
This is a classic. Visually, XSLT looks like XML, so it feels “almost correct.” But without a proper namespace, xsl:* elements won’t be recognized as commands, and the engine will either fail or do something meaningless.
Mistake #3: expecting formatting not to affect the output.
If you do method="text", line breaks in the XSLT literally become part of the result. And if you do method="xml", indentation and line breaks will depend on settings and on the engine (for example, indent="yes" may behave differently). That’s why you often need to normalize the string (trim()) or carefully control the template.
Mistake #4: building XSLT as a huge Kotlin string without a real need.
A triple-quoted string is a great learning option, but in real life maintaining a “wall of XSLT inside Kotlin code” is hard: quotes, indentation, escaping—and debugging turns into a quest. It’s much calmer to store XSLT in a separate .xslt file and read it as a resource/file.
Mistake #5: not separating the “XSLT compiles” stage from the “transformation runs” stage.
When everything is wrapped into a single try/catch, you lose clarity about what broke: a corrupted XSLT file or unsuitable input XML. Even splitting into two steps (newTransformer(...) separately, transform(...) separately) makes diagnostics an order of magnitude nicer.
GO TO FULL VERSION