Semantic validation of structured data using assertion-based rules.
Try it in your browser -- no install needed.
See the examples for a full walkthrough of every feature.
Download from GitHub Releases:
# Linux x86_64 (static musl)
curl -LO https://github.com/rh-jfuller/scheck/releases/latest/download/scheck-<version>-x86_64-unknown-linux-musl.tar.gz
tar xzf scheck-*-x86_64-unknown-linux-musl.tar.gz
sudo install scheck /usr/local/bin/
# macOS (Apple Silicon)
curl -LO https://github.com/rh-jfuller/scheck/releases/latest/download/scheck-<version>-aarch64-apple-darwin.tar.gz
tar xzf scheck-*-aarch64-apple-darwin.tar.gz
sudo install scheck /usr/local/bin/
sudo rpm -i https://github.com/rh-jfuller/scheck/releases/latest/download/scheck-<version>-1.x86_64.rpm
cargo install scheck
Use scheck-action to run scheck in your CI pipeline:
- uses: rh-jfuller/scheck-action-v1@v1
with:
document: config.json
rules: rules/security.scheck$ scheck --help
Semantic validation of structured data using assertion-based rules
Usage: scheck <COMMAND>
Commands:
validate Validate a document against a rule file
check Parse and validate a rule file (check for syntax errors)
convert Convert rules from another format to scheck JSON
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print version
$ scheck validate --help
Validate a document against one or more rule files
Usage: scheck validate [OPTIONS] --rules <RULES> <DOCUMENT>
Arguments:
<DOCUMENT> Document to validate (JSON or YAML); use "-" to read from stdin
Options:
-r, --rules <RULES> Rule file(s) -- repeat for multiple independent rulesets
--rule-format <RULE_FORMAT> Rule format (auto-detected from extension if omitted)
[possible values: dsl, json, schematron, freetext]
-p, --phase <PHASE> Phase to activate (default: schema's `default_phase`)
-c, --context <CONTEXT> Validate only a document subtree at given path
-f, --format <FORMAT> Output format [default: text]
[possible values: text, json, sarif]
--fail-on <FAIL_ON> Minimum severity that causes a non-zero exit code
[default: error]
[possible values: fatal, error, warning, info, never]
-h, --help Print help
Read the document from stdin with -:
$ cat advisory.json | scheck validate - --rules csaf-checks.json
scheck validate returns a distinct exit code so CI pipelines can gate on results:
| Code | Meaning |
|---|---|
0 |
No findings at or above the --fail-on threshold |
1 |
Findings at or above the --fail-on threshold |
2 |
Tool error (I/O, parse failure, bad arguments) |
The threshold defaults to error, so warnings and infos do not fail the build.
Use --fail-on warning to be stricter, or --fail-on never to always exit 0
regardless of findings (report-only mode).
Multiple --rules flags run each ruleset independently against the same
document, combining all findings into one report:
$ scheck validate advisory.json \
--rules rulesets/security/csaf-2.0-mandatory.json \
--rules rulesets/security/redhat-csaf-vex.json \
--phase full
$ scheck check --help
Parse and validate a rule file (check for syntax errors)
Usage: scheck check [OPTIONS] <RULES>
Arguments:
<RULES> Rule file to validate
Options:
--rule-format <RULE_FORMAT> Rule format (auto-detected from extension if omitted)
[possible values: dsl, json, schematron, freetext]
-h, --help Print help
$ scheck convert --help
Convert rules from another format to scheck JSON
Usage: scheck convert --from <FROM> <INPUT>
Arguments:
<INPUT> Input rule file to convert
Options:
--from <FROM> Source format [possible values: spectral]
-h, --help Print help
| Variable | Default | Description |
|---|---|---|
SCHECK_MAX_FILE_SIZE |
10485760 (10 MiB) |
Maximum input file size in bytes for both rule files and documents |
Schema validation only brings you so far.
JSON Schema, XML Schema, YAML validators -- they tell you whether a document is structurally correct. The right types in the right places. But they cannot tell you whether the data makes sense.
Real-world data has constraints that cut across structure - security data for example:
- A vulnerability advisory must contain a CVE ID, and that ID must match
CVE-YYYY-NNNNN+. - If a remediation references a vulnerability, that vulnerability must exist in the document.
- Every product in a branch must have at least one version range.
- A
statusfield set to"final"must not coexist with an emptyrelease_date. - An SBOM must contain at least one package with a valid PURL.
These are semantic rules -- co-occurrence constraints, cross-reference checks, conditional requirements, cardinality bounds. No schema language can express them. You end up writing bespoke validation code, scattered across your codebase, with inconsistent error messages and no way to share rules across teams.
scheck lets you define semantic assertions as rules -- in JSON, in Rust, or in a lightweight DSL -- then run them against any JSON or YAML document. Rules are data. Any language can generate them, any tool can read them.
Path expressions use JSONPath (RFC 9535), the standardized query language for JSON. No custom path syntax to learn.
scheck is inspired by ISO Schematron, the rule-based validation language for XML that Rick Jelliffe described as "a feather duster to reach the parts other schema languages cannot reach."
JSON Schema validates structure. scheck validates semantics. They are complementary: run JSON Schema first, then scheck.
Spectral pioneered "JSONPath + rules as data" for API linting. scheck
differs in three ways: (1) assert/report duality from Schematron --
Spectral has no notion of positive findings, (2) phases for selective
validation, (3) a typed rule model that round-trips through Rust, JSON,
DSL, and Schematron XML. If you already have Spectral rules,
scheck convert translates them.
Rego is a policy language -- expressive but requires learning a new syntax. scheck rules are data: JSON that any language can generate and non-developers can review. The tradeoff is expressiveness for portability. If your checks need loops or aggregation, use Rego. If they need to be reviewed by a compliance team, use scheck.
Format validators like csaf-rs implement specs exhaustively. scheck does
not replace them. scheck handles the checks you add on top:
organizational conventions, ingestion invariants, cross-document coherence.
The primary rule format is JSON. Every scheck schema serializes cleanly:
{
"title": "CSAF Advisory Checks",
"patterns": [
{
"name": "required-fields",
"title": "Core fields must be present",
"rules": [
{
"context": "$",
"checks": [
{
"kind": "assert",
"test": { "type": "exists", "path": "$.document" },
"message": "Document root must contain a 'document' object"
},
{
"kind": "assert",
"test": { "type": "exists", "path": "$.vulnerabilities" },
"message": "Advisory must contain at least one vulnerability",
"severity": "warning"
}
]
}
]
},
{
"name": "cve-format",
"title": "CVE identifiers must be well-formed",
"rules": [
{
"context": "$..vulnerabilities[*]",
"checks": [
{
"kind": "assert",
"test": {
"type": "matches",
"path": "$.cve",
"pattern": "^CVE-\\d{4}-\\d{4,}$"
},
"message": "CVE ID must match standard format"
}
]
}
]
}
]
}$ scheck validate advisory.json --rules csaf-checks.json
Or from any language -- generate the JSON, call scheck.
For Rust projects, the builder API gives you type-checked rule construction with no parsing at all:
use scheck::builder::*;
use scheck::{Severity, validate_json};
let rules = ruleset("CSAF Checks")
.pattern("required-fields", |p| p
.title("Core fields must be present")
.rule("$", |r| r
.assert(exists("$.document"), "must have document")
.assert(is_email("$.contact"), "invalid contact email")
.assert_with(
exists("$.vulnerabilities"),
"must have vulnerabilities",
Severity::Warning,
)
)
)
.build();
// Validate directly
let report = validate_json(&rules, &json_string)?;
assert!(report.is_ok());
// Or serialize to JSON for sharing
let rules_json = serde_json::to_string_pretty(&rules)?;Rulesets round-trip through JSON. Build in Rust, export as JSON for other tools. Import JSON, validate in Rust.
Convert existing Spectral rulesets to scheck JSON (one-shot, no runtime dependency):
$ scheck convert spectral-rules.yaml --from spectral > rules.json
Rules using truthy, pattern, length, and undefined convert directly.
Rules using custom JS functions are skipped with a comment. The output is a
standard scheck JSON ruleset you can edit, extend, and validate with.
scheck ships with starter rulesets under rulesets/ for
common organizational policy checks. These complement format-specific
validators (like csaf-rs or cyclonedx-bom) rather than replacing them:
| Domain | Directory | Rulesets |
|---|---|---|
| Security | rulesets/security/ |
CSAF 2.0, CycloneDX, SPDX, VEX, OSV |
| API | rulesets/api/ |
REST response contracts, JSON:API |
| Config | rulesets/config/ |
Kubernetes pod policy, GitHub Actions |
| Data Quality | rulesets/data-quality/ |
Contact records, dataset metadata |
$ scheck validate advisory.json --rules rulesets/security/csaf-2.0-mandatory.json --phase full
$ scheck validate response.json --rules rulesets/api/jsonapi.json
$ scheck validate pod.yaml --rules rulesets/config/kubernetes-pod.json
$ scheck validate contacts.json --rules rulesets/data-quality/contact-records.json
See rulesets/README.md for the full catalog. Each
subdirectory has its own README with details, phases, and limitations.
Borrowed directly from Schematron. Two kinds of checks:
assert-- the test must be true. If it fails, emit the message as a failure.report-- if the test is true, emit the message as a positive finding.
Assert catches problems. Report surfaces facts.
Group patterns into named sets and run only what matters:
{
"title": "checks",
"default_phase": "quick",
"phases": [
{ "name": "quick", "active_patterns": ["required-fields"] },
{ "name": "full", "active_patterns": ["required-fields", "cve-format"] }
],
"patterns": [ ]
}$ scheck validate doc.json --rules checks.json --phase full
Reusable explanations referenced by ID:
{
"diagnostics": [
{ "id": "spec-4.2", "message": "See CSAF 2.0 spec, section 4.2" }
]
}Every check carries a severity (fatal, error, warning, info) and an
optional flag for categorization.
Validate only a subtree of a document with --context:
$ scheck validate doc.json --rules rules.json --context '$.vulnerabilities[*]'
Rules run against each node matched by the context path instead of the full document root.
For Rust library users, Validated guarantees at the type level that a
document passed all checks:
use scheck::builder::*;
use scheck::try_validate;
let rules = ruleset("example")
.pattern("p", |p| p
.rule("$", |r| r.assert(exists("$.name"), "name required"))
)
.build();
let doc = scheck::from_json(r#"{"name": "Alice"}"#)?;
match try_validate(&rules, doc) {
Ok(validated) => {
let doc = validated.document(); // proven valid
}
Err(failed) => {
eprintln!("{}", failed.report().to_text());
}
}scheck uses JSONPath (RFC 9535):
| Syntax | Meaning |
|---|---|
$ |
Root of the document |
$.child |
Direct child named "child" |
$..name |
Recursive descent -- any descendant named "name" |
$.items[*] |
All elements of an array |
$.items[0] |
First element of an array |
$..book[?@.price < 10] |
Filter expression |
| Predicate | Meaning |
|---|---|
exists |
Node at path exists |
not_exists |
Node at path must not exist |
equals |
Scalar at path equals value |
matches |
Scalar at path matches regex |
count |
Count of nodes satisfies comparison |
named |
Value matches a built-in type (see below) |
and |
Both predicates must hold |
or |
At least one must hold |
not |
Predicate must not hold |
Built-in validators for common formats, usable as {"type": "named", "name": "<type>", "path": "..."}:
| Name | Matches |
|---|---|
email |
Email address (user@host.tld) |
url |
HTTP(S) URL |
cve_id / cve-id |
CVE identifier (CVE-YYYY-NNNNN+) |
purl |
Package URL (pkg:type/name) |
cpe |
CPE identifier (v2.2/v2.3) |
semver |
Semantic version (X.Y.Z, with optional pre-release/build) |
uuid |
UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) |
iso_date / iso-date |
ISO 8601 date (YYYY-MM-DD) |
iso_datetime / iso-datetime |
ISO 8601 datetime (YYYY-MM-DDThh:mm:ss) |
- text (default) -- human-readable, one line per finding
- json -- structured SVRL-inspired report with
fired-rules,failed-assert,successful-report - sarif -- SARIF v2.1.0 for GitHub Code Scanning and other tooling
use scheck::builder::*;
// Builder API with named types
let rules = ruleset("example")
.pattern("p", |p| p
.rule("$", |r| r
.assert(exists("$.name"), "name required")
.assert(is_email("$.email"), "invalid email")
.assert(is_semver("$.version"), "invalid version")
)
)
.build();
let report = scheck::validate_json(&rules, r#"{"name": "Alice"}"#)?;
// Load JSON rules directly
let report = scheck::check_json(rules_json, doc_json)?;
// Run multiple independent rulesets against one document
let report = scheck::validate_all(&[&base_rules, &org_rules], &doc);