Worked example
One schema, two documents, and the real output of every command on this
page. The files are examples/invoice.sch, examples/invoice-good.xml and examples/invoice-bad.xml in the repository.
The schema
Three patterns, two phases
The schema checks an invoice three ways, and each way is its own pattern — because patterns do not compete, while rules inside one pattern do.
examples/invoice.sch (abridged)
<schema xmlns="http://purl.oclc.org/dsdl/schematron" defaultPhase="basic">
<title>Invoice rules</title>
<phase id="basic">
<active pattern="structure"/>
</phase>
<phase id="strict">
<active pattern="structure"/>
<active pattern="lines"/>
<active pattern="totals"/>
</phase>
<let name="tax-rate" value="0.2"/>
<pattern id="structure">
<title>Structure</title>
<rule context="invoice">
<assert test="@id" flag="error">An invoice must have an id.</assert>
<assert test="total" flag="error">An invoice must have a total.</assert>
<assert test="count(line) > 0" flag="error">An invoice must have at least one line.</assert>
<report test="count(line) > 100" flag="info">
This invoice has <value-of select="count(line)"/> lines, which is unusual.
</report>
</rule>
</pattern>
<pattern id="lines">
<title>Line rules</title>
<!-- These two rules are alternatives, on purpose: a discount line is
checked by the first rule and never reaches the second. -->
<rule context="line[@type='discount']">
<assert test="number(@amount) < 0" flag="error" diagnostics="amount-help">
A discount line must have a negative amount, but <name/> has <value-of select="@amount"/>.
</assert>
</rule>
<rule context="line">
<assert test="@qty" flag="error">Every line needs a qty.</assert>
<assert test="number(@qty) > 0" flag="error" diagnostics="qty-help">
Quantity must be positive, but is <value-of select="@qty"/>.
</assert>
<assert test="number(@amount) >= 0" flag="error">
A normal line must not have a negative amount; use type="discount" for that.
</assert>
</rule>
</pattern>
<pattern id="totals">
<title>Totals</title>
<rule context="invoice">
<let name="expected" value="sum(line/@amount) * (1 + $tax-rate)"/>
<assert test="number(total) >= $expected - 0.01 and number(total) <= $expected + 0.01"
flag="warning">
Total is <value-of select="total"/> but the lines plus tax come to <value-of select="$expected"/>.
</assert>
</rule>
</pattern>
<diagnostics>
<diagnostic id="qty-help">
Quantity is the number of units ordered. It must be a positive number.
</diagnostic>
<diagnostic id="amount-help">
Amount is the line total in the invoice currency. Discounts are negative.
</diagnostic>
</diagnostics>
</schema>Note where $tax-rate and $expected live. A
schema-level let is a constant; a rule-level let is evaluated with the rule's context node, so $expected means something different for each invoice.
The documents
One that passes, one that does not
examples/invoice-good.xml
<?xml version="1.0" encoding="UTF-8"?>
<invoice id="INV-1001">
<line qty="2" amount="10.00"/>
<line qty="1" amount="5.00"/>
<line type="discount" qty="1" amount="-5.00"/>
<total>12.00</total>
</invoice>examples/invoice-bad.xml
<?xml version="1.0" encoding="UTF-8"?>
<invoice>
<line qty="-2" amount="10.00"/>
<line type="discount" qty="1" amount="5.00"/>
<total>99.00</total>
</invoice>Four things are wrong with the second one. Count them before reading on.
Run it
The default phase checks structure only
$ schematron -s examples/invoice.sch examples/invoice-bad.xml
examples/invoice-bad.xml:
error /invoice[1]
An invoice must have an id.
1 finding: 1 failed assert, 0 reports
$ echo $?
1One finding — because defaultPhase="basic" activates only the structure pattern. The line and total problems are still
there; nothing was asked to look for them.
$ schematron -s examples/invoice.sch --list-phases
Phases:
basic (default)
strict
Also accepted: #ALL, #DEFAULTRun it
The strict phase finds all four
$ schematron -s examples/invoice.sch -p strict examples/invoice-bad.xml
examples/invoice-bad.xml:
error /invoice[1]
An invoice must have an id.
error /invoice[1]/line[1]
Quantity must be positive, but is -2.
- Quantity is the number of units ordered. It must be a positive number.
error /invoice[1]/line[2]
A discount line must have a negative amount, but line has 5.00.
- Amount is the line total in the invoice currency. Discounts are negative.
warning /invoice[1]
Total is 99.00 but the lines plus tax come to 18.
4 findings: 4 failed asserts, 0 reportsThe first column is each finding's @flag. Lines beginning - are the diagnostics the assertions referenced. And the good
document, for contrast:
$ schematron -s examples/invoice.sch -p strict examples/invoice-good.xml
examples/invoice-good.xml:
no findings
$ echo $?
0Look closely at line[2].
The discount line has qty="1" and a positive amount. It was
caught by the discount rule, not the general line rule — because line[@type='discount'] comes first in the
pattern and claimed the node. That is first-matching-rule-wins doing
exactly what it was written to do.
Diagnose
What the schema will do, before it runs
--explain prints the compiled schema. It needs no document,
and it is the fastest way to see a rule that can never fire.
$ schematron -s examples/invoice.sch --explain
Invoice rules
query binding: (default, XPath 1.0)
namespaces: none declared
pattern structure
Structure
rule 1 context invoice
assert @id
assert total
assert count(line) > 0
report count(line) > 100
pattern lines
Line rules
rule 1 context line[@type='discount']
assert number(@amount) < 0
rule 2 context line
(only fires on nodes no earlier rule claimed)
assert @qty
assert number(@qty) > 0
assert number(@amount) >= 0
pattern totals
Totals
rule 1 context invoice
assert number(total) >= $expected - 0.01 and number(total) <= $expected + 0.01--verbose answers the other half — why did this fire?
— by printing the test and the rule behind each finding:
$ schematron -s examples/invoice.sch -p strict -v examples/invoice-bad.xml
examples/invoice-bad.xml:
error /invoice[1]
An invoice must have an id.
test: @id
rule: invoice
error /invoice[1]/line[1]
Quantity must be positive, but is -2.
test: number(@qty) > 0
rule: line
- Quantity is the number of units ordered. It must be a positive number.
error /invoice[1]/line[2]
A discount line must have a negative amount, but line has 5.00.
test: number(@amount) < 0
rule: line[@type='discount']
- Amount is the line total in the invoice currency. Discounts are negative.
warning /invoice[1]
Total is 99.00 but the lines plus tax come to 18.
test: number(total) >= $expected - 0.01 and number(total) <= $expected + 0.01
rule: invoice
4 findings: 4 failed asserts, 0 reportsAn empty list of fired rules here is the signature of the other common failure: a missing namespace prefix. Diagnosing that →
Filter
Fail on errors, still show warnings
--flag filters what is reported, and therefore what counts
toward the exit code. The total mismatch above is flagged warning, so it disappears here:
$ schematron -s examples/invoice.sch -p strict --flag error examples/invoice-bad.xml
examples/invoice-bad.xml:
error /invoice[1]
An invoice must have an id.
error /invoice[1]/line[1]
Quantity must be positive, but is -2.
- Quantity is the number of units ordered. It must be a positive number.
error /invoice[1]/line[2]
A discount line must have a negative amount, but line has 5.00.
- Amount is the line total in the invoice currency. Discounts are negative.
3 findings: 3 failed asserts, 0 reports
$ echo $?
1That is the shape of a build gate: fail on error, and run a
second unfiltered pass to surface warnings without breaking the build.