Building an XML Processing Pipeline in 2026

My original XML pipeline post is old enough that some of the tooling advice no longer matches what I’d actually recommend today. This is a refresh covering the same core pipeline shape — validation, transformation, output — but with the tools, libraries, and a few hard-won lessons updated for what I’m actually running on client projects now.

The problem

I get enough questions referencing my original XML processing pipeline post that it’s worth revisiting properly rather than patching it with comments. The core shape of the pipeline hasn’t changed — validate input, transform, validate output, deliver — but several of the specific tool choices I made back then aren’t what I’d reach for today, and a few problems I glossed over then deserve more honest treatment now.

The pipeline shape, unchanged

Regardless of which specific tools sit in each stage, the shape stays the same across every project I’ve built one of these for: validate the input against a schema before touching it, transform with XSLT (or occasionally a hand-written parser for cases XSLT genuinely isn’t the right tool for), validate the output against a second schema, and only then hand it off downstream. Skipping the input validation step is the single most common mistake I see in pipelines I’m asked to fix — malformed input produces a transform that either fails confusingly deep inside the XSLT or, worse, silently produces plausible-looking garbage.

<!– Minimal invocation shape — validate, transform, validate again –>

<pipeline>

  <validate schema=”input.xsd” file=”input.xml”/>

  <transform stylesheet=”convert.xsl” input=”input.xml” output=”output.xml”/>

  <validate schema=”output.xsd” file=”output.xml”/>

</pipeline>

I still build these as an explicit sequence of separately runnable steps rather than one monolithic script, specifically so each stage can be tested and debugged in isolation — this hasn’t changed, and I’d argue it matters more now than it did then, since pipelines have grown more complex on average.

Processor choice: still Saxon, but check which edition

My original post recommended Saxon for the transform stage, and that recommendation hasn’t changed — it’s still the most complete and best-maintained processor I use. What’s worth updating is which edition. If your pipeline needs any of the XSLT 3.0 features I’ve written about separately — maps, arrays, native JSON parsing — you need at minimum the free Home Edition running in 3.0 mode rather than whatever older Saxon jar might still be sitting in a build script from years back. Check the actual version pinned in your build tooling; I’ve fixed more than one pipeline that was silently running an ancient Saxon jar nobody had thought to update.

// Saxon invocation via the s9api interface — the API surface I’d

// point people to today rather than the older TransformerFactory route

Processor proc = new Processor(false);

XsltCompiler comp = proc.newXsltCompiler();

XsltExecutable exec = comp.compile(new StreamSource(new File(“convert.xsl”)));

XsltTransformer trans = exec.load();

trans.setSource(new StreamSource(new File(“input.xml”)));

Serializer out = proc.newSerializer(new File(“output.xml”));

trans.setDestination(out);

trans.transform();

The s9api interface is the one I’d steer people toward now over the older JAXP TransformerFactory approach — it exposes Saxon-specific features like schema-aware processing and better error reporting directly, instead of going through the generic interface that was designed around a lowest common denominator across processors.

Validation: don’t skip XSD versioning

One thing I underweighted in the original post: which XSD version your schemas are written against matters more than it looks like it should. XSD 1.1 added assertions (xs:assert) that let you express cross-field validation rules directly in the schema instead of pushing them into application code or Schematron. If your validator only supports XSD 1.0, those assertions are silently ignored rather than flagged as an error, which means a schema author can add a rule that looks like it’s enforced and isn’t.

<!– XSD 1.1 assertion — silently ignored by a 1.0-only validator,

     not rejected, which is the dangerous part –>

<xs:complexType name=”DateRange”>

  <xs:sequence>

    <xs:element name=”Start” type=”xs:date”/>

    <xs:element name=”End” type=”xs:date”/>

  </xs:sequence>

  <xs:assert test=”xs:date(End) ge xs:date(Start)”/>

</xs:complexType>

Confirm your validator’s XSD version explicitly rather than assuming — Saxon’s schema-aware processing (which requires the paid Enterprise Edition, worth noting since the Home Edition doesn’t include it) supports 1.1 assertions; many other validators still don’t, and the failure mode is quiet rather than loud.

Error handling: fail loud, fail early

The original post didn’t spend much time on what happens when a stage fails, which in hindsight was a gap. Every pipeline stage should fail with a specific, actionable message rather than letting an exception from three layers down bubble up unformatted. This matters most for the transform stage, where an unhandled runtime error in a template can otherwise produce a stack trace that says nothing about which input record triggered it.

// Wrap the transform call to attach context before rethrowing,

// so failures point at the actual input rather than a bare stack trace

try {

  trans.transform();

} catch (SaxonApiException e) {

  throw new PipelineException(

    “Transform failed processing ” + inputFile.getName() + “: ” + e.getMessage(), e);

}

This is a small amount of extra code that pays for itself the first time someone other than you has to debug a failed pipeline run at 2am without you available to explain what the raw exception actually means.

What I’d drop from the original post

In the interest of being honest about what’s changed: the original post spent time on a custom SAX-based streaming approach for large files that I no longer recommend as a starting point. XSLT 3.0’s formalized streaming mode does the same job with far less hand-rolled code, and unless you’re on a processor stuck at 2.0 or older, I’d reach for that first and only fall back to raw SAX handling for cases the streaming mode’s constraints genuinely can’t accommodate.

Conclusion

The pipeline shape — validate, transform, validate, deliver — hasn’t needed to change, but the tools underneath it have moved on enough that following the original post’s specific recommendations today would mean using an outdated Saxon edition and skipping XSD 1.1 entirely; check both before building on top of an old checklist, yours or anyone else’s.

FAQ

Is the free Saxon Home Edition enough for a production pipeline?

For most XSLT 3.0 transform work, yes. It doesn’t include schema-aware processing or XSD 1.1 assertion support, which requires the paid Enterprise Edition — decide whether you need that specifically before assuming the free edition covers everything.

Do I still need Schematron if I move to XSD 1.1?

Often not for the same rules — xs:assert covers a lot of what people used to reach for Schematron for. Schematron is still worth it for validation rules that are more naturally expressed as a pattern-and-message report than as an inline schema assertion, particularly when you want human-readable validation failure messages for non-developers.

Should I rewrite an existing 2.0-based pipeline just to get streaming?

Only if you’re actually hitting memory limits on large files today. If your input sizes are comfortable in memory, there’s no benefit to adopting 3.0’s streaming mode, and the constraints it puts on template structure aren’t worth taking on for no practical gain.

Why validate the output too, not just the input?

Because a transform bug can produce output that’s well-formed XML but doesn’t match what downstream systems expect, and that failure mode is much harder to catch after the fact than at the point the pipeline produced it. Output validation is cheap insurance against exactly that.

Leave a Reply

Your email address will not be published. Required fields are marked *