SHACL Validation
January 19, 2026 · View on GitHub
SETLr includes built-in support for validating RDF output using SHACL (Shapes Constraint Language), ensuring your generated data meets quality requirements.
Overview
SHACL validation enables:
- Schema validation of generated RDF
- Data quality checks
- Constraint enforcement
- Automated validation in workflows
- Validation reports
Basic SHACL Validation
Define shapes to validate your RDF output.
Simple Shape Example
@prefix setl: <http://purl.org/twc/vocab/setl/> .
@prefix prov: <http://www.w3.org/ns/prov#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.com/> .
# Data extraction and transformation
:input_table a csvw:Table ;
prov:wasGeneratedBy [ a setl:Extract ; prov:used <people.csv> ] .
:output a void:Dataset ;
prov:wasGeneratedBy [
a setl:Transform, setl:JSLDT ;
prov:used :input_table ;
prov:value '''[{
"@for": "row in input_table",
"@do": {
"@id": "http://example.com/person/{{row.ID}}",
"@type": "http://xmlns.com/foaf/0.1/Person",
"http://xmlns.com/foaf/0.1/name": "{{row.Name}}",
"http://xmlns.com/foaf/0.1/mbox": "mailto:{{row.Email}}"
}
}]'''
] .
# SHACL validation shape
:PersonShape a sh:NodeShape ;
sh:targetClass foaf:Person ;
sh:property [
sh:path foaf:name ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:datatype xsd:string ;
sh:minLength 1 ;
] ;
sh:property [
sh:path foaf:mbox ;
sh:minCount 1 ;
sh:pattern "^mailto:" ;
] .
# Apply validation
:output setl:hasShapesGraph :shapes_graph .
:shapes_graph prov:wasGeneratedBy [
a setl:Extract ;
prov:used <person-shapes.ttl>
] .
Validation in SETL Scripts
@prefix setl: <http://purl.org/twc/vocab/setl/> .
@prefix prov: <http://www.w3.org/ns/prov#> .
# Transform data
:output a void:Dataset ;
prov:wasGeneratedBy [
a setl:Transform, setl:JSLDT ;
prov:used :input ;
prov:value '''[...]'''
] ;
# Enable SHACL validation
setl:validateWithShapes :shapes .
# Load shapes from file
:shapes prov:wasGeneratedBy [
a setl:Extract ;
prov:used <shapes.ttl>
] .
SHACL Constraints
Required Properties
Ensure properties exist:
:PersonShape a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:property [
sh:path ex:name ;
sh:minCount 1 ; # Required
sh:message "Person must have a name"
] .
Cardinality Constraints
Control how many values:
sh:property [
sh:path ex:email ;
sh:minCount 1 ; # At least one
sh:maxCount 1 ; # At most one
] .
sh:property [
sh:path ex:phoneNumber ;
sh:minCount 0 ; # Optional
sh:maxCount 5 ; # Up to 5
] .
Datatype Constraints
Validate datatypes:
sh:property [
sh:path ex:age ;
sh:datatype xsd:integer ;
sh:minInclusive 0 ;
sh:maxInclusive 150 ;
] .
sh:property [
sh:path ex:email ;
sh:datatype xsd:string ;
sh:pattern "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$" ;
] .
sh:property [
sh:path ex:website ;
sh:nodeKind sh:IRI ; # Must be a URI
] .
Value Constraints
Restrict allowed values:
sh:property [
sh:path ex:status ;
sh:in ( "active" "inactive" "pending" ) ;
] .
sh:property [
sh:path ex:priority ;
sh:minInclusive 1 ;
sh:maxInclusive 10 ;
] .
String Constraints
Validate string patterns:
sh:property [
sh:path ex:zipCode ;
sh:pattern "^\\d{5}(-\\d{4})?$" ;
sh:flags "i" ; # Case insensitive
] .
sh:property [
sh:path ex:name ;
sh:minLength 2 ;
sh:maxLength 100 ;
] .
Class Constraints
Ensure correct types:
sh:property [
sh:path ex:creator ;
sh:class ex:Person ; # Must be a Person
] .
sh:property [
sh:path ex:organization ;
sh:or (
[ sh:class ex:Company ]
[ sh:class ex:Institution ]
) ;
] .
Validation Reports
Interpreting Reports
When validation fails, SETLr generates a detailed report:
from rdflib import Graph
import setlr
# Run SETL with validation
setl_graph = Graph()
setl_graph.parse('transform.setl.ttl', format='turtle')
try:
resources = setlr.run_setl(setl_graph)
print("Validation passed!")
except setlr.ValidationError as e:
print("Validation failed!")
print(e.report) # Access validation report
# Report contains:
# - sh:result - Individual violations
# - sh:focusNode - Node that failed
# - sh:resultPath - Property that failed
# - sh:resultMessage - Error message
Example Validation Report
@prefix sh: <http://www.w3.org/ns/shacl#> .
[ a sh:ValidationReport ;
sh:conforms false ;
sh:result [
a sh:ValidationResult ;
sh:focusNode <http://example.com/person/123> ;
sh:resultPath <http://xmlns.com/foaf/0.1/name> ;
sh:resultSeverity sh:Violation ;
sh:resultMessage "Person must have a name" ;
sh:sourceConstraintComponent sh:MinCountConstraintComponent ;
sh:sourceShape :PersonShape ;
]
] .
Advanced SHACL Features
Conditional Constraints
Use sh:or, sh:and, sh:not:
:UserShape a sh:NodeShape ;
sh:targetClass ex:User ;
# Must have email OR phone
sh:or (
[ sh:property [ sh:path ex:email ; sh:minCount 1 ] ]
[ sh:property [ sh:path ex:phone ; sh:minCount 1 ] ]
) .
Qualified Cardinality
Count specific types:
sh:property [
sh:path ex:author ;
sh:qualifiedValueShape [ sh:class ex:Person ] ;
sh:qualifiedMinCount 1 ; # At least one author that is a Person
] .
Property Pair Constraints
Compare two properties:
:DateRangeShape a sh:NodeShape ;
sh:targetClass ex:Event ;
sh:property [
sh:path ex:startDate ;
sh:lessThan ex:endDate ; # Start before end
] .
Closed Shapes
Restrict to only defined properties:
:PersonShape a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:closed true ;
sh:ignoredProperties ( rdf:type ) ;
sh:property [ sh:path ex:name ] ;
sh:property [ sh:path ex:email ] ;
# Any other property is invalid
.
Custom Messages
Provide helpful error messages:
sh:property [
sh:path ex:age ;
sh:datatype xsd:integer ;
sh:minInclusive 0 ;
sh:message "Age must be a non-negative integer"@en ;
] .
Severity Levels
Set constraint severity:
sh:property [
sh:path ex:email ;
sh:minCount 1 ;
sh:severity sh:Violation ; # Hard failure
] .
sh:property [
sh:path ex:phone ;
sh:minCount 1 ;
sh:severity sh:Warning ; # Warning only
] .
sh:property [
sh:path ex:fax ;
sh:maxCount 1 ;
sh:severity sh:Info ; # Information
] .
Validation in Python
Manual Validation
from rdflib import Graph
from pyshacl import validate
# Load data graph
data_graph = Graph()
data_graph.parse('data.ttl', format='turtle')
# Load shapes graph
shapes_graph = Graph()
shapes_graph.parse('shapes.ttl', format='turtle')
# Validate
conforms, report_graph, report_text = validate(
data_graph,
shacl_graph=shapes_graph,
inference='rdfs', # Enable RDFS inference
abort_on_first=False, # Find all violations
meta_shacl=False, # Don't validate shapes themselves
debug=False
)
if conforms:
print("✓ Validation passed")
else:
print("✗ Validation failed")
print(report_text)
Validation with Inference
Enable reasoning during validation:
from pyshacl import validate
conforms, report_graph, report_text = validate(
data_graph,
shacl_graph=shapes_graph,
inference='rdfs', # RDFS inference
# inference='owlrl', # OWL RL inference
# inference='both', # Both RDFS and OWL RL
)
Advanced Validation Options
from pyshacl import validate
conforms, report_graph, report_text = validate(
data_graph,
shacl_graph=shapes_graph,
ont_graph=ontology_graph, # Separate ontology
inference='rdfs',
abort_on_first=False, # Continue after first violation
allow_infos=True, # Include info-level results
allow_warnings=True, # Include warnings
meta_shacl=True, # Validate the shapes themselves
advanced=True, # Enable SHACL-AF features
js=True, # Enable JavaScript constraints
debug=True # Verbose output
)
Practical Examples
Example 1: Person Validation
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.com/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
ex:PersonShape a sh:NodeShape ;
sh:targetClass foaf:Person ;
sh:property [
sh:path foaf:name ;
sh:minCount 1 ;
sh:datatype xsd:string ;
sh:minLength 1 ;
sh:message "Every person must have a non-empty name"
] ;
sh:property [
sh:path foaf:mbox ;
sh:maxCount 1 ;
sh:pattern "^mailto:[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$" ;
sh:message "Email must be valid mailto: URI"
] ;
sh:property [
sh:path foaf:age ;
sh:maxCount 1 ;
sh:datatype xsd:integer ;
sh:minInclusive 0 ;
sh:maxInclusive 150 ;
] .
Example 2: Organizational Hierarchy
ex:OrganizationShape a sh:NodeShape ;
sh:targetClass ex:Organization ;
sh:property [
sh:path ex:hasEmployee ;
sh:class ex:Person ;
] ;
sh:property [
sh:path ex:parentOrganization ;
sh:maxCount 1 ;
sh:class ex:Organization ;
] ;
sh:property [
sh:path ex:legalName ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:datatype xsd:string ;
] .
ex:PersonShape a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:property [
sh:path ex:worksFor ;
sh:maxCount 1 ;
sh:class ex:Organization ;
] .
Example 3: Temporal Data
ex:EventShape a sh:NodeShape ;
sh:targetClass ex:Event ;
sh:property [
sh:path ex:startDate ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:datatype xsd:dateTime ;
] ;
sh:property [
sh:path ex:endDate ;
sh:maxCount 1 ;
sh:datatype xsd:dateTime ;
] ;
# End date must be after start date
sh:sparql [
sh:message "End date must be after start date" ;
sh:prefixes ex: ;
sh:select """
SELECT $this
WHERE {
$this ex:startDate ?start ;
ex:endDate ?end .
FILTER (?end <= ?start)
}
"""
] .
Best Practices
1. Design Shapes Early
Define validation requirements before generating data:
- Document expected schema
- Create shapes alongside SETL scripts
- Test with sample data
2. Use Meaningful Messages
sh:message "Email is required and must be valid"@en ;
3. Layer Validations
# Core validation (hard requirements)
:CorePersonShape sh:severity sh:Violation .
# Quality checks (warnings)
:QualityPersonShape sh:severity sh:Warning .
# Recommendations (info)
:OptimalPersonShape sh:severity sh:Info .
4. Test Shapes Independently
# Test shapes separately from SETL
shapes_graph = Graph()
shapes_graph.parse('shapes.ttl')
test_data = Graph()
test_data.parse('test-data.ttl')
conforms, _, report = validate(test_data, shacl_graph=shapes_graph)
assert conforms, f"Validation failed: {report}"
5. Version Control Shapes
- Store shapes with your SETL scripts
- Version them together
- Document changes
Integration Patterns
CI/CD Validation
# GitHub Actions
- name: Validate RDF Output
run: |
python -c "
from rdflib import Graph
from pyshacl import validate
data = Graph()
data.parse('output.ttl')
shapes = Graph()
shapes.parse('shapes.ttl')
conforms, _, report = validate(data, shacl_graph=shapes)
if not conforms:
print(report)
exit(1)
"
Pre-Production Checks
def validate_before_load(data_graph, shapes_graph, endpoint):
"""Validate data before loading to production"""
conforms, _, report = validate(data_graph, shacl_graph=shapes_graph)
if not conforms:
raise ValueError(f"Validation failed:\n{report}")
# Load to production endpoint
load_to_sparql(data_graph, endpoint)
Troubleshooting
Common Issues
Issue: Shapes not found
# Ensure shapes are loaded correctly
shapes_graph = Graph()
print(f"Loaded {len(shapes_graph)} triples from shapes file")
Issue: Validation too strict
# Use warnings for optional checks
sh:severity sh:Warning ;
Issue: Performance problems
# Validate in batches for large datasets
batch_size = 10000
for batch in batches(data_graph, batch_size):
validate(batch, shacl_graph=shapes_graph)
Related Documentation
- Advanced Features - Integration patterns
- Python API - Programmatic usage
- Examples - Complete examples
External Resources
Support
For SHACL-related questions:
- Open a discussion
- Report issues on GitHub