Calling XSLT from Java, .NET and Python
Compile the stylesheet once, pass parameters in, and treat any stylesheet that came from outside as executable code.
The three APIs
import javax.xml.transform.*;
import javax.xml.transform.stream.*;
public class Run {
// Compile once at startup; a Templates object is not thread-safe, so use a pool
static final Templates TEMPLATE = load();
static Templates load() {
try {
TransformerFactory f = TransformerFactory.newInstance();
// Route to Saxon when you need XSLT 2.0 or 3.0
// f = new net.sf.saxon.TransformerFactoryImpl();
f.setFeature(javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING, true);
return f.newTemplates(new StreamSource(new java.io.File("transform.xsl")));
} catch (TransformerException e) { throw new IllegalStateException(e); }
}
public static void main(String[] args) throws Exception {
Transformer t = TEMPLATE.newTransformer();
t.setParameter("locale", "en-GB");
t.transform(new StreamSource(new java.io.File("input.xml")),
new StreamResult(new java.io.File("out.html")));
}
}using System.Xml;
using System.Xml.Xsl;
var xslt = new XslCompiledTransform();
xslt.Load("transform.xsl", new XsltSettings(enableDocumentFunction: false,
enableScript: false),
resolver: null);
var args = new XsltArgumentList();
args.AddParam("locale", "", "en-GB");
using var reader = XmlReader.Create("input.xml");
using var writer = XmlWriter.Create("out.html",
new XmlWriterSettings { Indent = false });
xslt.Transform(reader, args, writer);from lxml import etree
# Compile once, then call it many times
transform = etree.XSLT(etree.parse("transform.xsl"))
for path in ["a.xml", "b.xml"]:
result = transform(etree.parse(path), locale="en-GB")
if not result: # an empty tree means the transform failed
raise RuntimeError(str(transform.error_log))
open(path.replace(".xml", ".html"), "wb").write(bytes(result))| Concern | Java | .NET | Python lxml |
|---|---|---|---|
| Engine version | 1.0 by default, Saxon optional | 1.0 only | 1.0 via libxslt |
| Compiled object reuse | Templates | XslCompiledTransform | etree.XSLT |
| Thread safety | Create a Transformer per thread | Not documented as safe; synchronise | Not thread-safe, one per thread |
| Parameters | setParameter | XsltArgumentList | Keyword arguments |
| Extensions | Java calls via Xalan/Saxon | msxsl:script | lxml extension functions |
Hardening an untrusted stylesheet
- A stylesheet can read files:
document()in the standard set is an arbitrary-file-read primitive. Disable it when the stylesheet is not yours. - Extension functions can execute code. On .NET disable
enableScript; on Java disable external function resolution. - Set secure processing, which caps entity expansion and disallows access to external resources.
- Set a transform timeout: a pathological stylesheet can be quadratic, and a shared worker pool will not protect you.
- Cache compiled templates keyed by file path plus modification time, and cap the cache.
from lxml import etree
# A parser that refuses to fetch or expand anything, for untrusted XML
parser = etree.XMLParser(resolve_entities=False, no_network=True,
load_dtd=False, huge_tree=False)
doc = etree.parse("uploaded.xml", parser)
transform = etree.XSLT(etree.parse("trusted.xsl"))
result = transform(doc)⚠️
Compiling a stylesheet on every request is the most common performance mistake in XSLT-backed services. Compilation costs milliseconds to tens of milliseconds and is pure overhead once the file stops changing — compile at startup and reuse the Templates object.
FAQ
How do I use XSLT 2.0 from Java?
Put saxon-he on the classpath and set the transformer factory explicitly, either with the system property javax.xml.transform.TransformerFactory or by instantiating Saxon's factory directly.
Why is the output empty in lxml?
lxml returns an empty result rather than raising on transform failure. Check transform.error_log, which holds the message and the line number in the stylesheet.
Related
XSLT versions, engines and where transformations run Debugging and testing stylesheets
Last refreshed 2026-09-18.