Examples: 09-severity.ttl.
Severity
sh:property [
s2c:name "person-name-capitalised" ;
sh:path ex:name ;
sh:pattern "^[A-Z]" ;
sh:severity sh:Warning ;
sh:message "Name {?value} should start with a capital letter" ;
] ;
sh:property [
sh:path ex:nickname ;
sh:maxLength 20 ;
sh:severity sh:Info ;
] .
Every constraint has a severity: sh:Violation (the default), sh:Warning or sh:Info. Severity doesn't change what is checked, only how much a failure matters. --fail-on decides what fails a run:
--fail-on | Exit code 1 when a rule fails at |
|---|---|
violation (default) | Violation |
warning | Violation or Warning |
info | any severity |
Custom severities, IRIs other than the three standard ones, count as violations.
Messages
sh:property [
sh:path ex:name ;
sh:minCount 1 ;
sh:message "Person {$this} has no name" ;
] ;
Without sh:message, rows say something like ex:PersonShape violates sh:minCount on ex:name. With it, you choose the wording. {$this} and {?value} are replaced at query time with the actual focus node and value.
Names you can rely on
Every rule has two identifiers:
ruleIdis structural:ex:PersonShape/ex:name/sh:minCount. It is derived from the shape, path and constraint, so moving shapes around or reordering a file doesn't change it.nameis short:PersonShape.name.minCount.s2c:namereplaces the shape-and-path part, so the pattern rule above is namedperson-name-capitalised.pattern.
The manifest also records a fingerprint per rule, a hash of what the rule means. If someone changes a pattern but keeps the name, the fingerprint changes. Commit the manifest and the change shows up in review.
Switching rules off
ex:LegacyShape a sh:NodeShape ;
sh:targetClass ex:LegacyAccount ;
sh:deactivated true ;
sh:property [ sh:path ex:migratedAt ; sh:minCount 1 ] .
A deactivated shape still compiles into the manifest, with status: deactivated and no queries, and shows as skipped in reports. It is visibly parked, not silently deleted. Because shapes files are unioned, you can deactivate a shape from a separate file without touching the original.
A rollout strategy
- Add new rules as
sh:Warning. Validate with the default--fail-on violation. The pipeline stays green, and the report shows how much data breaks the rule. - Fix the data, or the rule.
- Remove the severity so the rule becomes a violation and guards the graph from then on.
In the pipeline
Compile when shapes change, so the reviewed manifest is exactly what runs. Validate on a schedule, or after each data load:
name: graph-validation
on:
pull_request:
paths: ['shapes/**']
schedule:
- cron: '0 5 * * *'
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cargo install --locked shacl2cypher --features neo4j # or download a release binary
- run: shacl2cypher compile shapes/*.ttl --dialect neo4j --node-key id --strict -o checks
- run: git diff --exit-code checks # manifest committed and current
- run: shacl2cypher validate --manifest checks/manifest.json --connect ${{ secrets.BOLT_URI }} --timeout 120 --format sarif -o report.sarif
env:
NEO4J_PASSWORD: ${{ secrets.NEO4J_PASSWORD }}
- uses: github/codeql-action/upload-sarif@v3
if: always()
with: { sarif_file: report.sarif }
What each piece buys you:
- Exit codes:
0clean,1violations at or above--fail-on,2a setup problem,3a query timed out and conformance is unknown. Treat3as a failure: an incomplete check is not a pass. - SARIF puts each violation on the line of the shapes file that declared the constraint, where code-scanning tools can show it.
- JUnit (
--format junit) gives one test case per rule for test dashboards: failures for violations, errors for timeouts. --timeoutbounds each query. Expensive rules, with cost classesunbounded-pathorquadratic, are the ones to watch. The manifest'srecommendedIndexeslists the label and key pairs worth indexing.
Where to go from here
You now know enough SHACL to describe almost any rule a property graph needs. For the full mapping rules and recipes, read the SHACL guide. To generate shapes from a visual model instead of writing them by hand, see LPG Modeler. Every option is listed in the reference.
Takeaways
- Severity grades failures;
--fail-ondecides which ones break the build. sh:messagewith{$this}/{?value}makes rows readable;s2c:namemakes names stable and meaningful.- Commit the manifest, validate with it, and publish JUnit or SARIF.