The sbol-rs validators answer a specific question: does this SBOL
document conform to the spec? There is one validator for SBOL 3
(sbol3) and one for SBOL 2 (sbol2); they share the same reporting
types, the same ValidationConfig, and the same rule-classification
taxonomy from sbol-core. The honest answer has more shape than yes/no,
and this document explains what shape that takes for both versions.
Every spec rule is in a per-version catalog (crates/sbol3/rules.toml,
149 rules; crates/sbol2/rules.toml, 268 rules) with a status, severity,
gate, and (where applicable) a blocker. When you call Document::check()
(SBOL 3) or Document::validate() (SBOL 2), the validator runs every
algorithm it has against your document and returns a ValidationReport
carrying three things:
- Issues: actionable diagnostics for things that are demonstrably
wrong (
Error) or recommended-but-not-required violations (Warning). Each issue has a stable rule ID, subject IRI, optional property name, message, and optional hint. - Coverage: per-rule signal saying whether each spec rule was
fully evaluated, partially evaluated (with a
coverage_kindexplaining the scope limit), or not evaluated (with a reason). - Applied options: what configuration produced this report, so the result is reproducible.
This is intentionally more than a boolean. The validator's job is twofold: surface what's wrong AND tell the caller what was checked.
The validator is honest about its scope. SBOL 3.1.0 has 149 validation rules, but they are not all the same shape. Appendix B (p.2837–2840) marks 40 rules with the ▲ symbol: weak-REQUIRED conditions that the spec itself says are NOT to be machine-reported. Of the remaining 109 machine-checkable rules, all 109 are fully implemented (100%). The SBOL 2.3.0 catalog is larger (268 rules, 29 of them ▲), and of its 239 machine-checkable rules all 239 are fully implemented. The SBOL 3 conformance report and the SBOL 2 conformance report carry the per-rule details, and the freshness of both is enforced in CI.
The validator surfaces this distinction at runtime via the coverage signal:
fully_applied: every spec case for this rule is decidable with the current configuration and was evaluated against this document.partially_applied: the local subset ran but the rule's full coverage is bounded by configuration (no resolver) or by the bundled ontology snapshot (out-of-snapshot terms). Each entry carries acoverage_kindnaming the scope.not_applied: the rule isUnimplemented(no algorithm yet) orMachineUncheckable(spec ▲ guidance).
If you want "validation passed = spec-compliant" with no footnotes, use
Document::check_complete() (or sbol validate --treat-partial-as-errors). This returns Err whenever
partially_applied is non-empty, so any partial coverage trips the
gate.
Every rule classifies into exactly one of five statuses. The first three are implementations; the last two record what's missing.
| Status | Count | What it means |
|---|---|---|
Error |
55 | Algorithm complete; MUST violations emit as Severity::Error. |
Warning |
17 | Algorithm complete; SHOULD violations emit as Severity::Warning. |
Configurable |
37 | Algorithm complete; behavior varies with configuration. The blocker field names which axis: Resolver, Ontology (snapshot-bounded), Policy (Conservative/Strict/Lenient modes), or External (local-only mode; full spec scope unreachable per-document). |
MachineUncheckable |
40 | Spec ▲: violations are not to be machine-reported. May have a local subset that emits warnings on positively-decidable cases. |
Unimplemented |
0 | No local algorithm yet. The blocker field names what's needed (Ontology data, Resolver protocol, Policy decision). |
The counts above are the SBOL 3 catalog; SBOL 2 uses the same five
statuses (Error 166, Warning 30, Configurable 43, ▲ MachineUncheckable
29). The full per-rule grids live in conformance.md and
sbol2-conformance.md. They are generated from the
catalogs; regenerate after any change to a validation_rule_statuses()
with:
cargo run -p sbol3 --bin generate-conformance-report
cargo run -p sbol2 --bin generate-sbol2-conformance-report
Both validators read the same ValidationConfig (from sbol-core),
which selects which rule families run at all, orthogonal to per-rule
overrides and policy modes, which control severity within the running
families. Its defaults match libSBOLj's SBOLValidate command-line
defaults:
| Flag | Default | Effect |
|---|---|---|
compliant |
on | Run the compliant-URI structural family. |
complete |
on | Run the completeness family: every referenced object must be present. |
best_practice |
off | Run the SHOULD-level best-practice family. |
types_in_uri |
off | Interpret compliant URIs as carrying an optional type segment. |
keep_going |
on | Continue past the first error and collect every issue. |
ValidationConfig::all_on() enables every family;
ValidationConfig::default() runs compliant + complete.
The SBOL 2 catalog binds each rule to a gate naming the family that
runs it (Always, Compliant, Complete, or BestPractice), so the
flag model is directly observable per rule. This is what the SBOLTestSuite
SBOL 2 corpora exercise: the valid SBOL2 corpus passes under both the
default and all_on; SBOL2_ic fails only when complete is on;
SBOL2_nc fails only when compliant is on; and SBOL2_bp is flagged
(at warning severity) only when best_practice is on. The gate counts and
this behavior are tabulated in
sbol2-conformance.md.
use sbol::prelude::*;
let document = Document::read_path("design.ttl")?;
// Run everything; inspect the full report.
let report = document.validate();
for issue in report.issues() { ... }
for partial in &report.coverage().partially_applied { ... }
// Err on any Error-severity issue.
document.check()?;
// Err if any rule's coverage is partial (strict CI gate).
document.check_complete()?;The SBOL 2 surface mirrors this through sbol2 (or sbol::v2):
Document::validate() returns the same ValidationReport, and
Document::validate_with_config(&config) takes the shared
ValidationConfig to select gates:
use sbol2::validation::ValidationConfig;
let document = sbol2::Document::read_path("design.xml")?;
let report = document.validate_with_config(&ValidationConfig::all_on());The full options surface (per-rule overrides, severity floor/ceiling,
policy modes, resolver context) is on ValidationOptions and
ValidationContext. Use Document::check_with(options) or
Document::check_with_context(context) to pass them:
let options = ValidationOptions::default()
.deny("sbol3-12807")? // promote to error
.allow("sbol3-10502")? // suppress
.with_severity_floor(Severity::Error); // warnings → errors
let context = ValidationContext::with_options(options)
.with_external_mode(ExternalValidationMode::ProvidedOnly)
.with_document_resolver(&resolver);
document.check_with_context(context)?;# Basic
sbol validate design.ttl
# JSON output for CI consumption
sbol validate design.ttl --format json --treat-partial-as-errors
# SARIF for GitHub code scanning
sbol validate design.ttl --format sarif --output report.sarif
# Per-rule overrides
sbol validate design.ttl --allow sbol3-10502 --deny sbol3-12807
# Resolver mode for cross-document reference checks
sbol validate design.ttl --external-mode provided --resolve-documents ./bundleRun sbol validate --help for the full flag surface.
For a CI gate that fails on any spec gap, use check_complete /
--treat-partial-as-errors:
# .github/workflows/validate.yml
- run: |
sbol validate design.ttl \
--format sarif \
--treat-partial-as-errors \
--output validation.sarif
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: validation.sarifExit code 0 means "every rule was fully evaluated and zero errors." Anything else trips the gate; see validation-output.md for the full code table.
The validator emits three formats, all from the same
ValidationReport:
- text (default): one line per issue plus a one-line summary, plus
an optional coverage summary with
--show-coverage. - JSON v1: versioned schema documented in
validation-output.md. The serializer is
hand-written so the core crate stays free of
serde_jsonin its public dependency tree; the round-trip test undercrates/sbol3/tests/validation_output.rsuses serde_json as a test-only dep to verify every emitted field parses. - SARIF v2.1.0: for GitHub code scanning and editor extensions.
Built into
sbol-cli. SBOL coverage metadata is carried inruns[].invocations[0].properties.coverageso SARIF consumers that recognize it surface it; consumers that don't ignore it per the SARIF spec.
All three formats carry the same data: issues, coverage, applied options.
Three knobs let callers tune validation without forking the catalog:
- Per-rule overrides:
ValidationOptions::allow(rule)suppresses the rule,deny(rule)promotes its emit to Error,warn(rule)demotes to Warning. CLI flags:--allow,--deny,--warn. - Severity floor / ceiling: promote all warnings to errors with
with_severity_floor(Severity::Error)(or--severity-floor error), or demote errors to warnings with a ceiling. - Policy modes:
PolicyOptionscarriesConservative/Strict/Lenientknobs for rules where the spec is ambiguous. Each knob has a committed ADR under policies/. The default is alwaysConservativeand matches current emit behavior; opt-in modes are explicit.
The validator does not check what cannot be checked from a single document. Three explicit boundaries:
- Global IRI uniqueness (sbol3-10101): no per-document validator
can prove an IRI is globally unique. The validator does
document-local uniqueness; the rule is classified
Configurablewith theExternalblocker so its scope limit is visible in coverage rather than hidden behind a green light. - ▲ rules (40 of them): the spec says "violations are not to be
machine-reported." The validator runs a local subset where one is
possible and emits warnings on positively-decidable cases (never
errors). The broader spec rule is always recorded in coverage as
MachineUncheckable. - Out-of-snapshot ontology terms: the bundled ontology snapshot
(
crates/sbol-ontology, ~12,000 terms across SBO, SO, EDAM, GO, ChEBI) covers terms from the spec's Tables 1–17 plus their relevant subtrees. Custom or out-of-snapshot terms remain undecided by design; the spec doesn't define what they mean and the validator doesn't guess.
Adding a new rule (or refining an existing one) is a three-step change,
shown here for SBOL 3; SBOL 2 mirrors it under crates/sbol2/:
- Catalog: add or update the entry in
crates/sbol3/rules.toml. The build script gates status/blocker/severity invariants and (for Policy rules) requires a committed ADR underdocs/policies/. - Algorithm: implement the check in
crates/sbol3/src/validation/rules/<section>.rs, dispatched fromValidator::validateincrates/sbol3/src/validation/validator.rs. - Tests: add at least one regression case under
crates/sbol3/tests/rule_cases/<section>.rs. The meta-testimplemented_validation_rules_have_regression_casesfails the build if a rule with an algorithm lacks one.
Regenerate the conformance grid (cargo run -p sbol3 --bin generate-conformance-report, or the sbol2 peer); CI's
git diff --exit-code enforces freshness.
See testing.md for the full test architecture.
| Topic | Doc |
|---|---|
| SBOL 3 per-rule status grid (generated) | conformance.md |
| SBOL 2 per-rule status grid (generated) | sbol2-conformance.md |
| JSON v1 output schema | validation-output.md |
| Policy ADRs (one per ambiguous rule) | policies/ |
| Test architecture | testing.md |
| RDF I/O | rdf-io.md |