Your first validation¶
In this tutorial you write a SHACL shapes file and a data file, validate one against the other, read the report, and fix the data. It takes about ten minutes and assumes no prior SHACL knowledge — only that you can read Turtle, RDF’s text format, well enough to recognise a triple when you see one.
Install¶
Wheels for pyshifty are published to PyPI and contain a pre-compiled
engine, so installing it needs no Rust toolchain:
pip install "pyshifty[rdflib]"
The distribution is named pyshifty but the module is shifty:
import shifty
For the command-line tool, download an archive from GitHub Releases and put the extracted
shifty executable on your PATH. Linux x86-64, Windows x86-64, and macOS
arm64 archives are built by the release workflow. To build from source instead,
use Rust:
git clone https://github.com/gtfierro/shifty
cd shifty
cargo install --path crates/shifty-cli
This tutorial uses the CLI for the first half and Python for the second. If you want to use only Python, skip to Validate from Python — nothing in the CLI half is a prerequisite.
Create the shapes and data graphs¶
Create a working directory with two files in it. The first describes what a
valid person looks like. Call it shapes.ttl:
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:PersonShape a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:property [
sh:path ex:name ;
sh:minCount 1 ;
sh:datatype xsd:string ;
] ;
sh:property [
sh:path ex:email ;
sh:minCount 1 ;
] .
Read that as three separate claims. sh:targetClass ex:Person says which
nodes this shape applies to: every node typed ex:Person. The first
sh:property block says each of those nodes must have at least one
ex:name, and that every value found there must be a string. The second says
each must have at least one ex:email, with no constraint on the value.
The second file is the data to check. Call it data.ttl:
@prefix ex: <http://example.org/> .
ex:alice a ex:Person ; ex:name "Alice" ; ex:email "alice@example.org" .
ex:bob a ex:Person ; ex:name 123 .
Alice satisfies both obligations. Bob’s ex:name is an integer rather than a
string, and he has no ex:email. The validator reports these as distinct
reasons.
Run the validator¶
shifty validate --shapes shapes.ttl --data data.ttl
conforms: false — 1 violation in 2 findings
Finding 1 of 2
target class(ex:Person)
severity Violation
shape ex:PersonShape
failure at least 1 value(s) required along ex:email, found 0
path ex:email
found 0 value(s) along the path; at least 1 required
requirement ∃[1..] ex:email
affects ex:bob
value node (the focus node itself)
also fails Finding 2
Finding 2 of 2
target class(ex:Person)
severity Violation
shape ex:PersonShape
failure test(datatype(xsd:string)) not satisfied
path ex:name
requirement test(datatype(xsd:string))
affects ex:bob
value node "123"^^xsd:integer
also fails Finding 1
notation
∃[m..n] p . X between m and n values along p satisfy X
Alice is absent from the output. A validation report contains failures only. It does not distinguish between a node that passed validation and a node that was never selected by the shape. To inspect selection and passing evaluations, use the evidence interface; to extract selected nodes and values as bindings, use shape maps. The failure-explanation tutorial demonstrates the evidence workflow.
The CLI groups by finding: one for the missing email and one for the wrong
name datatype. Both affect Bob, so the summary says one violation in two
findings. value node identifies "123"^^xsd:integer for the datatype
failure; the missing email has no offending value. target class(ex:Person)
shows why Bob was checked. See How shapes are compiled for the
compiled constraint notation.
Fix the data¶
Edit data.ttl so Bob’s name is a string and he has an email:
@prefix ex: <http://example.org/> .
ex:alice a ex:Person ; ex:name "Alice" ; ex:email "alice@example.org" .
ex:bob a ex:Person ; ex:name "Bob" ; ex:email "bob@example.org" .
Run the same command again:
conforms: true
The quotes around "Bob" are doing real work here. In Turtle, 123 is an
xsd:integer and "123" is an xsd:string; they are different RDF
terms, and sh:datatype distinguishes them. Many SHACL datatype violations
come from this difference.
Validate from Python¶
shifty.validate takes the data graph first and the shapes graph second and
returns the same three kinds of values as pyshacl.validate. Check keyword
arguments when migrating an existing call; Python API reference lists
the accepted Shifty signature.
import pathlib
import shifty
conforms, report_graph, results_text = shifty.validate(
pathlib.Path("data.ttl"),
pathlib.Path("shapes.ttl"),
)
print(conforms)
print(results_text)
You get three things back. conforms is the boolean. report_graph is an
rdflib.Graph holding a W3C sh:ValidationReport, which is the
interoperable form to hand to another tool. results_text is that report
rendered for a human:
Validation Report
Conforms: False
Results (2):
Constraint Violation in DatatypeConstraintComponent
Severity: sh:Violation
Source Shape: _:...
Focus Node: <http://example.org/bob>
Result Path: <http://example.org/name>
Value: "123"^^<http://www.w3.org/2001/XMLSchema#integer>
Message: Value 123 does not have datatype <http://www.w3.org/2001/XMLSchema#string>
Constraint Violation in MinCountConstraintComponent
Severity: sh:Violation
Source Shape: _:...
Focus Node: <http://example.org/bob>
Result Path: <http://example.org/email>
Message: Fewer than 1 values on path <http://example.org/email>
This is the standard SHACL report vocabulary rather than the CLI’s compact
summary, so the two commands print different text for the same input. The
Source Shape is a blank-node identifier because the property shapes in
shapes.ttl were written inline with [ ... ] and so have no IRI of their
own. The blank-node identifiers and result order can vary between runs.
Note
This tutorial uses validate() because it returns the standard W3C report
vocabulary. Shifty also provides validate_algebra(), its native
structured result interface. See validation interfaces before choosing an interface for an
application.
Any of str (Turtle text), bytes, pathlib.Path, or an
rdflib.Graph works as an argument, and a list of them is merged first.
Passing paths is the fastest option, because the file is parsed in Rust without
a round-trip through rdflib.
Shapes and data graph inputs¶
You passed two files, and Shifty treated them asymmetrically: shapes were read
only from shapes.ttl. If data.ttl had contained a stray
sh:NodeShape — copied in from somewhere, or generated by an upstream tool —
it would have been ignored rather than quietly becoming a constraint.
If you pass just one graph, it plays both roles:
shifty validate --shapes combined.ttl
conforms, report, text = shifty.validate("combined.ttl")
This is the common case where shape definitions and instance data live in the same file. Shapes graphs and data graphs specifies the graph-input and visibility rules, including how to validate against shapes embedded in data.
Next steps¶
Reading validation results in code continues with this same graph, and moves from reading a report to writing code that consumes one.
For a specific job — running SHACL-AF rules, extracting bindings from a conforming node, or looking at how the shapes were compiled — the how-to guides are the shorter path.