Context and tests¶
A Schematron rule has two halves that work together. The context of a
<rule> is an XPath pattern that selects the nodes to
check; the test of each <assert> or <report> inside that rule is an XPath
boolean evaluated with the selected node as the current node. Because
the context node is the starting point, tests are written with relative
paths — cbc:LineExtensionAmount, cac:InvoiceLine, @currencyID — not
absolute paths from the document root.
The running example¶
This section validates a small public UBL Invoice. We will return to it on
every page:
Context selects, test checks¶
A rule with context="cac:InvoiceLine" fires once per invoice line. Inside
it, every test runs with that line as the current node, so a bare
cbc:LineExtensionAmount means "the amount of this line":
| line-rule.sch | |
|---|---|
- Relative path — the
cbc:LineExtensionAmountchild of this line. @currencyIDis the attribute on that child, addressed with the attribute axis. Both run with the matchedcac:InvoiceLineas the current node.
The rule fires twice (two cac:InvoiceLine nodes). Both lines have an amount
and a currencyID, so all four assertions hold — no message is reported.
assert vs report
An <assert> is the normal form: it reports its message when the test is
false (the expected condition failed). A <report> is the inverse — it
reports when the test is true (an unwanted condition was found). The
same XPath skills apply to both.
XPath 2.0 with queryBinding="xslt2"¶
The default Schematron binding is XPath 1.0. Declaring
queryBinding="xslt2" on the root <schema> unlocks XPath 2.0: sequences,
sum(), count(), string-length(), matches(), quantified
every/some $x in ... satisfies ..., and if/then/else. This is what makes
cross-field calculation rules expressible.
A totals-equal-sum check (the idea behind the real BR-CO-10) compares the document total against the sum of the line amounts — a single XPath 2.0 expression over a sequence:
sum(...)adds every line'scbc:LineExtensionAmountas a sequence...steps up fromcac:LegalMonetaryTotalto theInvoiceso the path reaches the siblingcac:InvoiceLineelements.
100.00 + 50.00 = 150.00 and the total is 150.00, so the test is true and the
assertion passes. Change the total to 140.00 and the message fires.
Common test idioms¶
Most business rules are built from a handful of XPath shapes. The current node is always the rule's context node.
| Intent | XPath test |
|---|---|
| Existence | cbc:ID |
| Non-empty value | normalize-space(cbc:CustomizationID) != '' |
| Conditional requirement ("if A then B") | not(A) or B |
| Cardinality (exactly one) | count(cac:LegalMonetaryTotal) = 1 |
| Code membership | cbc:DocumentCurrencyCode = ('EUR','USD','GBP') |
The non-empty idiom is exactly how the real BR-01 ("An Invoice shall have a
Specification identifier") is bound — its test is
normalize-space(cbc:CustomizationID) != ''.
If A then B = not(A) or B
XPath has no if … then requirement operator, so a conditional rule is
written as the logically equivalent not(condition) or requirement. Read it
as: "either the condition does not apply, or the requirement is met." This
one pattern is the heart of nearly every business rule.
Severity with flag¶
Each assertion may carry a flag attribute naming its severity. flag is a
conventional label — Schematron does not assign it meaning; the validation
report simply carries it through so a consumer can sort fatal failures from
advisory ones. The EN16931 rules use fatal and warning.
A fatal calculation rule — the authentic BR-CO-15, the invoice total with VAT equals the total without VAT plus the VAT amount:
A warning rule — the authentic BR-51 — uses <report> because it flags an
unwanted condition, and marks it warning rather than fatal:
A fatal failure typically blocks acceptance of the document; a warning is
advisory and surfaces as a non-blocking note. Both appear in the same report,
distinguished only by their flag.
The conditional-requirement pattern, worked¶
The not(A) or B shape is worth seeing in full, because it is how almost every
"only required when …" rule is expressed. Suppose a payment made by card must
carry an account identifier:
| conditional.sch | |
|---|---|
Ais "this is a card payment" (cbc:PaymentMeansCode = '48');Bis "an account identifier is present". WhenAis false the line passes automatically; whenAis true,Bmust hold.
- Non-card payment →
not(A)is true → assertion passes regardless ofB. - Card payment with an account id →
Atrue,Btrue → passes. - Card payment without an account id →
Atrue,Bfalse → fails and the message is reported.
Next¶
Variables and messages — name the values your
tests reuse with <let>, and turn a passing or failing test into a precise,
data-rich report message.