SHACL 1.2#

RDF 1.2 makes a statement into something you can describe, and SPARQL 1.2 makes it something you can query. The remaining question is how to require it: if your model says that every employment must record when it started and ended, something has to check that the annotations are there and well-formed.

That is what SHACL 1.2 adds, and this chapter covers the two constraint components rudof implements for it: sh:reificationRequired and sh:reifierShape.

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()

The data#

Three researchers and their employers, written with the RDF 1.2 annotation syntax. Each :employer statement should carry the years it covers:

  • :timbl has two complete spells at CERN;

  • :vint has a spell with a start year but no end year;

  • :nobody has an employer with no annotation at all.

rudof.read_data("""
prefix :     <http://example.org/>
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
prefix xsd:  <http://www.w3.org/2001/XMLSchema#>

:timbl a :Human ;
  rdfs:label "Tim Berners-Lee" ;
  :employer :CERN {| :start "1984"^^xsd:gYear ; :end "1994"^^xsd:gYear |}
                  {| :start "1980"^^xsd:gYear ; :end "1980"^^xsd:gYear |} .

:vint a :Human ;
  rdfs:label "Vinton Cerf" ;
  :employer :Google {| :start "2005"^^xsd:gYear |} .

:nobody a :Human ;
  rdfs:label "Nobody" ;
  :employer :ACME .
""")

Constraining the annotations#

A SHACL 1.2 shapes graph is an ordinary SHACL shapes graph with two extra keywords available on a property shape:

Keyword

Means

sh:reificationRequired true

every value of this path must have a reifier

sh:reifierShape :S

and every reifier must conform to the node shape :S

So the constraint reads: an :employer statement must be annotated, and the annotation must be an employment spell with exactly one :start and one :end.

rudof.read_shacl("""
prefix :    <http://example.org/>
prefix sh:  <http://www.w3.org/ns/shacl#>
prefix xsd: <http://www.w3.org/2001/XMLSchema#>

:Researcher a sh:NodeShape ;
  sh:targetClass :Human ;
  sh:property [
    sh:path :employer ;
    sh:nodeKind sh:IRI ;
    sh:reificationRequired true ;
    sh:reifierShape :EmploymentSpell
  ] .

:EmploymentSpell a sh:NodeShape ;
  sh:property [ sh:path :start ; sh:datatype xsd:gYear ;
                sh:minCount 1 ; sh:maxCount 1 ] ;
  sh:property [ sh:path :end   ; sh:datatype xsd:gYear ;
                sh:minCount 1 ; sh:maxCount 1 ] .
""")

Note that :EmploymentSpell is a plain SHACL node shape (nothing about it is specific to 1.2). The new part is only what it is applied to: not a node in the graph, but the reifier of a statement.

report = rudof.validate_shacl()

print("conforms:  ", report.conforms)
print("violations:", len(report))
conforms:   False
violations: 2

Two failures, and they are the two different ways this can go wrong. Each one names the constraint component that raised it, which is what tells them apart:

for entry in report:
    print(f"path:       {entry.path}")
    print(f"constraint: {entry.constraint_component}")
    print(f"message:    {entry.message}")
    print()
path:       http://example.org/end
constraint: http://www.w3.org/ns/shacl#MinCountConstraintComponent
message:    "MinCount(1) not satisfied"

path:       http://example.org/employer
constraint: http://www.w3.org/ns/shacl#ReifierShapeConstraintComponent
message:    "Reification required but no reifier found for triple {triple} with predicate {pred}"

References#