schematron 0.16.1

Schematron: pure Rust implementation of ISO/IEC 19757-3 rule-based XML validation, with its own XML parser, XPath 1.0 engine, XPath 2.0 and 3.0 subsets, schema linting, and SVRL reporting. By SixArm.com.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
# schematron

**Schematron in pure Rust.** Rule-based XML validation to ISO/IEC 19757-3,
with no `libxml2`, no XSLT processor, no C toolchain, no FFI, and no `unsafe`.

[![crates.io](https://img.shields.io/crates/v/schematron.svg)](https://crates.io/crates/schematron)
[![docs.rs](https://docs.rs/schematron/badge.svg)](https://docs.rs/schematron)
[![license](https://img.shields.io/crates/l/schematron.svg)](#license)
[![website](https://img.shields.io/badge/website-schematron--rust.github.io-8b3a1f)](https://schematron-rust.github.io/)

<https://schematron-rust.github.io/> is the same material as this README and
`spec/`, in a form you can link someone to: a
[worked example](https://schematron-rust.github.io/example/) of one schema and
two documents with the real output at each step, the
[report formats](https://schematron-rust.github.io/reports/) side by side, and
the [conformance table](https://schematron-rust.github.io/conformance/).

Every other route to Schematron in Rust goes through C: bind to `libxml2`, or
shell out to Saxon, or compile the schema into XSLT and find an XSLT engine to
run it. This crate does none of that. It contains its own XML parser, its own
XPath engine — a complete XPath 1.0 implementation plus documented XPath 2.0
and 3.0 subsets — and its own validator, and it *interprets* a schema
directly rather than transpiling it.

---

## What Schematron is for

DTD, XML Schema, and RELAX NG describe the **shape** a document may take.
Schematron describes the **conditions** a document must satisfy, written as
XPath expressions — so it can express what a grammar cannot:

- co-occurrence rules — "if `@type` is `invoice`, then `total` is required"
- value relationships — "`end` must not precede `start`"
- cross-references between distant parts of a document
- cardinality that depends on content rather than position

Schematron is normally layered *on top of* a grammar, not used instead of one.

```xml
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
  <pattern>
    <rule context="invoice">
      <assert test="total">An invoice must have a total.</assert>
      <report test="count(line) &gt; 100">This invoice has an unusual number of lines.</report>
    </rule>
  </pattern>
</schema>
```

Read that as: *for every `invoice` element, assert that it has a `total`
child.* `assert` fires when its test is **false**; `report` fires when its
test is **true**.

---

## Install

```sh
cargo add schematron
```

Or the command line tool:

```sh
cargo install schematron
```

---

## Library

```rust
use schematron::{Document, Schema};

fn main() -> schematron::Result<()> {
    // Compiling is the expensive step: it resolves includes, expands
    // abstractions, and parses every XPath expression in the schema.
    let schema = Schema::from_path("rules.sch")?;
    let document = Document::from_path("data.xml")?;
    let report = schema.validate(&document)?;

    if report.is_valid() {
        println!("valid");
    } else {
        for failure in report.failures() {
            println!("{}: {}", failure.location, failure.text);
        }
    }
    Ok(())
}
```

A report is **data**, not formatted text, so one run renders three ways:

```rust
let svrl = report.to_svrl();   // SVRL, for other Schematron tooling
let json = report.to_json().unwrap();  // JSON, keeping the tree structure
let text = report.to_text();   // for a person
```

It can also be queried directly, instead of scraped back out of text:

```rust
report.is_valid();                  // no assert failed
report.count_failures();            // how many did
report.with_flag("error").count();  // findings the schema flagged as errors
report.count_fired_rules();         // zero here means NO context matched
```

Patterns are independent, so a schema with several of them can evaluate them
on separate threads. The report is identical either way — this is a
performance switch, not a behaviour switch:

```rust
let options = ValidateOptions::new().with_parallel_patterns(true);
let report = schema.validate_with(&document, &options)?;
```

Measure before turning it on: on a small document the threads cost more than
they save. See [spec/validation/](spec/validation/index.md).

`Schema` is immutable and `Send + Sync`. Compile once, validate in parallel:

```rust
use std::sync::Arc;

let schema = Arc::new(Schema::from_path("rules.sch")?);

for path in paths {
    let schema = Arc::clone(&schema);
    std::thread::spawn(move || {
        let document = Document::from_path(path)?;
        schema.validate(&document)
    });
}
```

---

## Command line

```sh
schematron --schema rules.sch data.xml
schematron -s rules.sch -p strict -f svrl -o report.svrl data.xml
schematron -s rules.sch --flag error docs/*.xml
cat data.xml | schematron -s rules.sch -
```

| Exit code | Meaning |
|---|---|
| 0 | every document valid |
| 1 | at least one failed assertion |
| 2 | usage error |
| 3 | schema error |
| 4 | document error |

Two flags worth knowing when a schema is misbehaving:

```sh
schematron -s rules.sch --explain          # what the compiled schema will do
schematron -s rules.sch --verbose data.xml # which rules actually fired
```

Full option list: [spec/cli/](spec/cli/index.md).

---

## The one rule to internalise

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

Rules in one pattern compete like the arms of a match expression. This is a
feature — it is how you write "otherwise" branches:

```xml
<pattern>
  <rule context="line[@type='discount']">
    <assert test="number(@amount) &lt; 0">A discount must be negative.</assert>
  </rule>
  <rule context="line">
    <assert test="number(@amount) &gt;= 0">A normal line must not be negative.</assert>
  </rule>
</pattern>
```

— and it is also the most common way to write a schema that silently does
nothing:

```xml
<pattern>
  <rule context="*">…</rule>
  <rule context="invoice">…</rule>  <!-- never runs: * claimed everything -->
</pattern>
```

To apply independent checks to the same node, put them in **separate
patterns**. Patterns do not compete; each gets its own pass over the document.

The second-most common cause of a schema that does nothing is a missing
namespace prefix. XPath 1.0 has no default namespace, so an unprefixed name
matches elements in *no* namespace. Declare a prefix with `<ns>` and use it
everywhere. `--verbose` shows an empty list of fired rules when this is what
has happened.

---

## What is implemented

A summary. [spec/conformance/](spec/conformance/index.md) is authoritative, and
states the limits and deliberate divergences in full.

| Area | Status |
|---|---|
| `schema`, `pattern`, `rule`, `assert`, `report` | Full |
| `ns`, `let`, `phase`, `active`, `include`, `extends` | Full |
| Abstract patterns (`abstract`, `is-a`, `param`) | Full |
| Abstract rules (`rule/@abstract` + `extends`) | Full |
| `diagnostics`, `properties`, `value-of`, `name`, `emph`, `span`, `dir` | Full |
| `@flag`, `@role`, `@subject`, `@see`, `@icon`, `@fpi` | Full |
| `pattern/@documents` | Full |
| `key` and `key()` | Full, as a non-ISO extension — see [spec/keys/]spec/keys/index.md |
| Phases, `#ALL`, `#DEFAULT`, `@defaultPhase` | Full |
| SVRL output | Full |
| XPath 1.0 — 13 axes, 27 core functions, exact conversion semantics | Full |
| XPath `document()`, with cross-document node-sets | Full |
| XPath 2.0 sequences, dates, durations, type operators, value and node comparisons, `for`, `some`, `every`, ranges, regular expressions | Subset — see [spec/xpath2/]spec/xpath2/index.md |
| XPath 3.0 function items, `=>`, `\|\|`, `!`, `let`, `Q{uri}local`, `for-each()`, `filter`, `fold-left`, `fold-right`, `for-each-pair`, `function-lookup`/`-arity`/`-name` | Subset — see [spec/xpath3/]spec/xpath3/index.md |
| `queryBinding="xslt"`, `"xpath"`, or absent | Supported |
| `queryBinding="xslt2"`, `"xpath2"` | Partly — see [spec/xpath2/]spec/xpath2/index.md |
| `queryBinding="xslt3"`, `"xpath3"` | Partly — see [spec/xpath3/]spec/xpath3/index.md |
| `queryBinding="xpath31"` and later | Refused by default |
| `extends rule` and `extends href`, with `#fragment` identifiers | Full |
| `document(uri, base)` | Full — resolves against the second argument's first node |
| Streaming validation (`--stream`) | Full, as a non-ISO extension, for schemas provably local to one record — see [spec/streaming/]spec/streaming/index.md |
| XPath 2.0 kind tests as node tests — `element()`, `attribute(id)`, `document-node()` | Full, under an `xslt2` binding |

Beyond the standard, the crate lints a schema for constructs that are legal
but almost certainly wrong — a rule shadowed by an earlier one in the same
pattern, an unprefixed name in a namespaced schema, a variable or key nothing
uses, a rule that reports nothing — starting with the two ways a Schematron
schema silently does nothing:

```sh
schematron --schema rules.sch --lint
```

See [spec/linting/](spec/linting/index.md).

A separate check asks a different question — **will this schema behave the
same under another Schematron processor?**

```sh
schematron --schema rules.sch --portability
```

What it reports is not wrong. It is correct, and works here, and the ISO
reference implementation treats it differently: a `let` shadowing an outer
one, which that implementation refuses outright; a rule on `comment()`, which
it never fires; a rule's `@flag`, which it drops. Each check is backed by a
divergence in [spec/conformance/](spec/conformance/index.md), established by
running both implementations against the same schema. Kept out of `--lint`,
because a linter that reports correct code gets switched off.

XPath 2.0 is a different language, not XPath 1.0 with extra functions. The
crate implements a documented subset of it — regular expressions,
conditionals, and the string and numeric functions schemas actually use — and
makes everything outside that subset a **hard error naming the construct**,
never a wrong answer. [spec/xpath2/](spec/xpath2/index.md) is explicit about what
is in, what is out, and the handful of places where a `xslt2` schema still
evaluates with XPath 1.0 semantics.

---

## Correctness

Beyond the test suite, the crate is compared against the **ISO Schematron
reference implementation** — the XSLT stylesheets that compile a schema into a
validator — over the whole corpus. Twenty of twenty-three cases agree exactly;
the other three are documented in
[spec/conformance/](spec/conformance/index.md#measured-against-the-reference-implementation),
and in each the difference is the reference's rather than this crate's.

```sh
sh tests/differential/fetch-skeleton.sh /tmp/skeleton
SCHEMATRON_SKELETON=/tmp/skeleton cargo test --test differential -- --ignored
```

## Security

The XML parser never resolves an external entity and never processes a DTD's
entity declarations. XXE is therefore **structurally impossible** here rather
than merely switched off; a reference to a DTD-declared entity is an error.

The default resolver reads local files and refuses `http:` and `https:` URIs.
Network access is something an application opts into by supplying its own
`Resolver`, never something the library does behind your back.

Parse depth, include depth, and expression nesting are all bounded, and
exceeding a bound returns an error rather than exhausting the stack. Four
`cargo-fuzz` targets exist to keep that true.

---

## Performance

Every XPath expression is parsed **once**, when the schema is compiled, and
the compiled schema is reused across documents and across threads. Rule
contexts are evaluated once per document rather than tested node by node, so
matching is linear rather than quadratic in document size. Patterns can
optionally evaluate in parallel.

For a document too large to comfortably hold fully in memory —
`--stream`/`Schema::validate_streaming` — validates one repeating record at
a time instead, for schemas whose rules are local to a record's own
subtree. Refused, naming why, rather than silently falling back to
ordinary validation, when a schema or document doesn't qualify. See
[spec/streaming/](spec/streaming/index.md).

Indicative figures, and the benchmarks that produce them, are in
[spec/testing/](spec/testing/index.md#benchmarks). Run them yourself with
`cargo bench` — the numbers in any README are somebody else's hardware.

---

## Documentation

[`index.md`](index.md) is the full map. [`AGENTS.md`](AGENTS.md) is the
contributor and agent guide.

The specification is in [`spec/`](spec/), is **normative**, and is written to
be read:

| Document | What it covers |
|---|---|
| [spec/index.md]spec/index.md | Overview and design principles |
| [spec/tutorial/]spec/tutorial/index.md | **Start here** — eighteen steps from first rule to real schema |
| [spec/data-model/]spec/data-model/index.md | Every Schematron element and its Rust type |
| [spec/validation/]spec/validation/index.md | The validation algorithm, exactly |
| [spec/xpath/]spec/xpath/index.md | The XPath 1.0 engine |
| [spec/xpath2/]spec/xpath2/index.md | The XPath 2.0 subset, and its limits |
| [spec/xpath3/]spec/xpath3/index.md | The XPath 3.0 subset: function items, `=>`, `\|\|`, `!`, `let`, `Q{uri}local`, the higher-order sequence functions |
| [spec/xml/]spec/xml/index.md | The XML parser and data model |
| [spec/parsing/]spec/parsing/index.md | The five compilation passes |
| [spec/svrl/]spec/svrl/index.md | The SVRL report format, read and written |
| [spec/keys/]spec/keys/index.md | Keys, and why a cross-reference check needs one |
| [spec/linting/]spec/linting/index.md | Catching schemas that silently do nothing |
| [spec/api/]spec/api/index.md | Library API |
| [spec/cli/]spec/cli/index.md | Command line interface |
| [spec/errors/]spec/errors/index.md | Error taxonomy |
| [spec/conformance/]spec/conformance/index.md | Limits and divergences, stated up front |
| [spec/testing/]spec/testing/index.md | Tests, fuzzing, benchmarks, lints |
| [spec/rust-msrv-n-minus-2/]spec/rust-msrv-n-minus-2/index.md | MSRV policy: current stable minus two |
| [spec/roadmap/]spec/roadmap/index.md | What is next |

If the code and the specification disagree, that is a defect in one of them.

Runnable examples:

```sh
cargo run --example validate_file        # the shortest useful program
cargo run --example report_formats       # SVRL, JSON, and text from one run
cargo run --example embedded_schema      # includes served from memory
cargo run --example parallel_validation  # one schema, eight threads
cargo run --example streaming_validation # bounded memory, one record at a time
cargo run --example xpath_engine         # the XPath engine on its own
```

---

## Development

```sh
cargo test --all-features                                   # everything
cargo clippy --all-targets --all-features -- -D warnings    # pedantic, and clean
cargo doc --no-deps --all-features                          # warning-free
cargo +1.96 test --all-features                             # the MSRV boundary
```

Slower, run when relevant: `cargo bench`, and
`cargo +nightly fuzz run fuzz_validate` (needs `cargo-fuzz`).

The conformance suite lives in `tests/corpus/`. Each case is a directory
holding `schema.sch`, `input.xml`, and `expected.txt`; adding a case means
adding a directory, with no Rust to change.

The documentation is machine-checked: `tests/docs.rs` compiles every schema
shown in the docs, resolves every relative link, and ties duplicated facts —
the MSRV, the CLI flags, the XPath function list — back to their single
source. See [agents/testing.md](agents/testing.md).

### Releases

An AI coding agent may decide a specific, already-landed, gate-passed
change warrants a release, and run the actual `cargo publish` for it — not
only prepare one for a human to run. The same decision also covers
committing, merging into `main`, and pushing `main` and the release tag to
`origin`, bounded by
[`../spec/release-process/`](../spec/release-process/index.md) and
disclosed in [`../AI_STATEMENT.md`](../AI_STATEMENT.md). The maintainer
remains accountable either way; see both documents for exactly what that
does and does not mean.

---

## License

Licensed under any of:

- MIT License
- Apache License, Version 2.0
- GNU General Public License version 2 only
- GNU General Public License version 3 only

at your option.