The big one

My schema runs, and finds nothing

schematron -s rules.sch --lint

Cause 1: a missing namespace prefix

XPath 1.0 has no default namespace, so an unprefixed name matches elements in no namespace. Against a namespaced document every context fails to match and nothing fires.

schematron -s rules.sch --verbose --phase '#ALL' data.xml

An empty list of fired rules is the signature. The fix is to declare a prefix with <ns> and use it everywhere:

<ns prefix="inv" uri="http://example.com/invoice"/>
<rule context="inv:invoice">
  <assert test="inv:total">An invoice must have a total.</assert>
</rule>

Cause 2: an earlier rule claimed the nodes

Within a single pattern, each node is processed by at most one rule: the first whose context matches it.

schematron -s rules.sch --explain

--explain marks every rule after the first in a pattern with a reminder that it only sees nodes no earlier rule claimed. The fix is to move the independent checks into separate patterns — patterns do not compete, and each gets its own pass over the document.

In your own tests, assert on it.

report.count_fired_rules() returning zero is the programmatic form of this symptom. Check it and a broken schema fails loudly instead of passing silently.

Frequently asked

Other things that surprise people

My assert fires when the document looks fine.

assert fires when its test is false; report fires when its test is true. If a check fires on the good document and stays quiet on the bad one, the two are swapped.

not(a = b) and a != b behave differently.

Correct, and both are sometimes what you want. Comparing a node-set with = in XPath 1.0 is existential: true if any node compares true. So not(line/@type = 'discount') means "no line is a discount", while line/@type != 'discount' means "some line is not a discount".

A cross-reference check is slow, or does not work.

A cross-reference needs a key. The crate implements key and key() as a documented non-ISO extension — spec/keys/ explains why a cross-reference check needs one, and what the alternative costs.

My schema uses an XPath 2.0 or 3.0 construct and the crate refuses it.

By design: outside the implemented subset you get a hard error naming the construct, rather than a plausible wrong answer. spec/xpath2/ and spec/xpath3/ list what is in and what is out for each.

An include over http: fails.

The default resolver refuses http: and https:, and the command line tool has no network flag. Vendor the included schema next to the one that includes it, or use the library with your own Resolver.

Turning on --parallel made things slower.

Likely, on a small document: the threads cost more than they save. Parallel patterns are a performance switch, not a behaviour switch — the report is identical either way, so measure and keep whichever is faster for your inputs.

A large document runs out of memory, or takes too much of it.

Try --stream (or Schema::validate_streaming): it validates one repeating record at a time — the document element's direct children, e.g. <order> under <orders> — instead of materialising the whole document. Only for a schema whose rules are local to one record's own subtree; anything else (key(), id(), document(), following::, a schema- or phase-scoped <let>, among others) is refused by name rather than silently falling back — a fallback that might exhaust the memory you were trying to avoid using in the first place. See spec/streaming/ for exactly what qualifies.

My schema works here but behaves differently under another processor.

Run schematron -s rules.sch --portability. It reports the constructs that are correct here and treated differently by the ISO reference implementation — a let that shadows an outer one, a rule on comment(), a rule's @flag.

Still stuck?

Where to ask

Open an issue on GitHub. A schema, an input document, and what you expected is almost always enough — and if the answer turns out to be a gap in the specification, that is a defect worth fixing too.

Open an issue Read the specification