SHACL#
This chapter is a short introduction to SHACL (Shapes Constraint Language) using rudof.
Preliminaries: install and configure rudof#
%pip install -q "pyrudof>=0.3.22"
Note: you may need to restart the kernel to use updated packages.
from pyrudof import Rudof
rudof = Rudof()
Shapes that are themselves RDF#
SHACL makes two different choices from ShEx:
Shapes are written with the
sh:vocabulary, in Turtle or any other RDF syntax. They can be stored, queried and validated like any other data.Because a shape is a node, it can carry its own targets:
sh:targetClass,sh:targetNode,sh:targetSubjectsOfandsh:targetObjectsOfsay which nodes the shape applies to. There is no separate shape map.
Let’s read some data:
rudof.read_data("""
prefix : <http://example.org/>
prefix xsd: <http://www.w3.org/2001/XMLSchema#>
prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
:timbl rdf:type :Human ;
:name "Tim Berners-Lee" ;
:birthPlace :london ;
:birthDate "1955-06-08"^^xsd:date ;
:employer :CERN ;
:knows _:1 .
:london rdf:type :city, :metropolis ;
:country :UK .
:CERN rdf:type :Organization .
_:1 :birthPlace :Spain .
""")
And a shapes graph describing it:
rudof.read_shacl("""
prefix : <http://example.org/>
prefix sh: <http://www.w3.org/ns/shacl#>
prefix xsd: <http://www.w3.org/2001/XMLSchema#>
:Person a sh:NodeShape ;
sh:targetClass :Human ;
sh:property [ sh:path :name ;
sh:datatype xsd:string ;
sh:minCount 1 ; sh:maxCount 1 ] ;
sh:property [ sh:path :birthDate ;
sh:datatype xsd:date ;
sh:maxCount 1 ] ;
sh:property [ sh:path :birthPlace ;
sh:node :Place ; sh:maxCount 1 ] ;
sh:property [ sh:path :employer ;
sh:node :Organization ] .
:Place a sh:NodeShape .
:Organization a sh:NodeShape .
""")
:Person is a node shape: it applies to a node as a whole. Its sh:targetClass :Human
says it applies to every node typed :Human, which is what makes validation self-directing.
Each sh:property is a property shape, describing the values reachable along one
sh:path:
sh:datatype,sh:minCountandsh:maxCountare the counterparts of ShEx’s datatype constraints and cardinalities.sh:nodeis the counterpart of ShEx’s@reference: the value must itself conform to the named shape.
Validating#
report = rudof.validate_shacl()
print("conforms:", report.conforms)
print("violations:", len(report))
conforms: True
violations: 0
validate_shacl returns a
ShaclValidationReport.
This data conforms, so there is nothing to look at yet. Let’s add some nodes that break the
shapes: :wrong1 has two birth dates where at most one is allowed, and :wrong2 has a
:name that is an integer rather than a string.
rudof.read_data("""
prefix : <http://example.org/>
prefix xsd: <http://www.w3.org/2001/XMLSchema#>
prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
:wrong1 rdf:type :Human ;
:name "John" ;
:birthDate "1955-06-08"^^xsd:date, "1956-06-08"^^xsd:date .
:wrong2 rdf:type :Human ;
:name 23 ;
:birthDate "1955-06-08"^^xsd:date .
""", merge=True)
report = rudof.validate_shacl()
print("conforms:", report.conforms)
print("violations:", len(report))
conforms: False
violations: 2
Each entry in the report is a validation result, and carries the pieces you need to locate and explain the failure:
Field |
Meaning |
|---|---|
|
the node that was being validated |
|
the property path whose values broke the constraint |
|
the offending value, when there is a single one |
|
the shape that raised it |
|
which constraint: |
|
|
|
a human-readable explanation |
for entry in report:
print(f"{entry.focus_node}")
print(f" path: {entry.path}")
print(f" value: {entry.value}")
print(f" constraint: {entry.constraint_component}")
print(f" severity: {entry.severity}")
print(f" message: {entry.message}")
print()
http://example.org/wrong1
path: http://example.org/birthDate
value: None
constraint: http://www.w3.org/ns/shacl#MaxCountConstraintComponent
severity: Violation
message: "MaxCount(1) not satisfied"
http://example.org/wrong2
path: http://example.org/name
value: "23"^^xsd:integer
constraint: http://www.w3.org/ns/shacl#DatatypeConstraintComponent
severity: Violation
message: "Expected Datatype: xsd:string"
Rendering the report#
serialize_shacl_validation_results renders the current results.
ResultShaclValidationFormat chooses the rendering and ShaclValidationSortMode the
ordering:
from pyrudof import ResultShaclValidationFormat, ShaclValidationSortMode
print([str(f) for f in ResultShaclValidationFormat.all()])
print([str(m) for m in ShaclValidationSortMode.all()])
['Details', 'Turtle', 'NTriples', 'RdfXml', 'TriG', 'N3', 'NQuads', 'Minimal', 'Compact', 'Json', 'Csv']
['Severity', 'Node', 'Component', 'Value', 'Path', 'SourceShape', 'Details']
print(rudof.serialize_shacl_validation_results(ResultShaclValidationFormat.Minimal))
Does not conform, 2 violations, 0 warnings
import re
# rudof's table renderings are written for a terminal, and make the severity IRI a
# clickable hyperlink using an escape sequence a notebook does not understand. Strip
# those; the colours are left alone, since those a notebook renders fine.
HYPERLINK = re.compile("\x1b\\]8;;.*?\x1b\\\\")
def as_text(rendered):
return HYPERLINK.sub("", rendered)
print(as_text(rudof.serialize_shacl_validation_results(
ResultShaclValidationFormat.Compact,
ShaclValidationSortMode.Node,
)))
╭──────────────┬─────────┬────────────────────────────────┬────────────┬───────────────────┬────────────────────────────────────╮
│ Severity │ Node │ Component │ Path │ Value │ Source shape │
├──────────────┼─────────┼────────────────────────────────┼────────────┼───────────────────┼────────────────────────────────────┤
│ sh:Violation │ :wrong1 │ sh:MaxCountConstraintComponent │ :birthDate │ │ _:bbc18e6eaece948f48dd5841b6aff7a6 │
├──────────────┼─────────┼────────────────────────────────┼────────────┼───────────────────┼────────────────────────────────────┤
│ sh:Violation │ :wrong2 │ sh:DatatypeConstraintComponent │ :name │ "23"^^xsd:integer │ _:b5c526f7ad2d129b4f9ed712f10a3b77 │
╰──────────────┴─────────┴────────────────────────────────┴────────────┴───────────────────┴────────────────────────────────────╯
The RDF renderings deserve a special mention. SHACL defines the validation report itself
as RDF, using the sh:ValidationReport and sh:ValidationResult vocabulary. So a report
can be stored in a graph, queried with SPARQL, or validated against a shape in turn:
print(rudof.serialize_shacl_validation_results(ResultShaclValidationFormat.Turtle))
@prefix sh: <http://www.w3.org/ns/shacl#> .
_:b0 sh:result _:b1 , _:b2 ;
a sh:ValidationReport ;
sh:conforms false .
_:b1 sh:sourceShape _:b3 ;
sh:focusNode <http://example.org/wrong1> ;
a sh:ValidationResult ;
sh:resultPath <http://example.org/birthDate> ;
sh:resultSeverity sh:Violation ;
sh:sourceConstraintComponent sh:MaxCountConstraintComponent ;
sh:resultMessage "MaxCount(1) not satisfied" .
_:b2 sh:sourceShape _:b4 ;
sh:focusNode <http://example.org/wrong2> ;
a sh:ValidationResult ;
sh:resultPath <http://example.org/name> ;
sh:resultSeverity sh:Violation ;
sh:sourceConstraintComponent sh:DatatypeConstraintComponent ;
sh:value 23 ;
sh:resultMessage "Expected Datatype: xsd:string" .
Validation modes#
rudof ships more than one SHACL engine, selected with ShaclValidationMode. Native runs
rudof’s own implementation; the SPARQL-based mode expresses the constraints as SPARQL
queries instead.
from pyrudof import ShaclValidationMode
print([str(m) for m in ShaclValidationMode.all()])
['Native', 'Sparql']
report = rudof.validate_shacl(ShaclValidationMode.Native)
print("conforms:", report.conforms, "| violations:", len(report))
conforms: False | violations: 2
Shapes and data in one graph#
Because shapes are RDF, a single file can perfectly well hold both the shapes and the
instances they describe. Calling read_shacl() with no input tells rudof to take the
shapes graph from the data already loaded in the session:
with Rudof() as session:
session.read_data("""
prefix : <http://example.org/>
prefix sh: <http://www.w3.org/ns/shacl#>
prefix xsd: <http://www.w3.org/2001/XMLSchema#>
:PersonShape a sh:NodeShape ;
sh:targetClass :Person ;
sh:property [ sh:path :name ; sh:datatype xsd:string ; sh:minCount 1 ] .
:alice a :Person ; :name "Alice" .
:bob a :Person .
""")
session.read_shacl() # no input: the shapes come from the loaded data
report = session.validate_shacl(ShaclValidationMode.Native)
print("conforms:", report.conforms)
for entry in report:
print(f" {entry.focus_node} — {entry.message}")
conforms: False
http://example.org/bob — "MinCount(1) not satisfied"
ShapesGraphSource is the enum that names this choice explicitly, for configurations that
need to state where the shapes come from rather than infer it:
from pyrudof import ShapesGraphSource
print([str(s) for s in ShapesGraphSource.all()])
['CurrentData', 'CurrentSchema']
Serializing a shapes graph#
A loaded shapes graph can be written back out in any RDF syntax, which is how you move shapes between tools that prefer different formats:
from pyrudof import ShaclFormat
print([str(f) for f in ShaclFormat.all()])
['Internal', 'Turtle', 'NTriples', 'RdfXml', 'TriG', 'N3', 'NQuads', 'JsonLd', 'Json']
with Rudof() as session:
session.read_shacl("""
prefix : <http://example.org/>
prefix sh: <http://www.w3.org/ns/shacl#>
:PersonShape a sh:NodeShape ;
sh:targetClass :Person ;
sh:property [ sh:path :name ; sh:minCount 1 ] .
""")
print(session.serialize_shacl(ShaclFormat.NTriples))
<http://example.org/PersonShape> <http://www.w3.org/ns/shacl#targetClass> <http://example.org/Person> .
<http://example.org/PersonShape> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/ns/shacl#NodeShape> .
<http://example.org/PersonShape> <http://www.w3.org/ns/shacl#property> _:b0 .
_:b0 <http://www.w3.org/ns/shacl#path> <http://example.org/name> .
_:b0 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/ns/shacl#PropertyShape> .
_:b0 <http://www.w3.org/ns/shacl#minCount> "1"^^<http://www.w3.org/2001/XMLSchema#integer> .
References#
SHACL - the W3C recommendation.
SHACL Advanced Features - SPARQL-based constraints, rules and node expressions.
Validating RDF Data - a book covering ShEx, SHACL and how the two compare.