SHACL: Validating Knowledge Graphs with Shapes

SHACL (Shapes Constraint Language) is the W3C standard for validating RDF data against shapes, a Recommendation since 2017. A shape says what a conforming node must look like: which properties it must have, how many values each may take, what type those must be. A processor checks a data graph against a shapes graph and returns a machine-readable report of every violation.

Why SHACL exists alongside OWL

SHACL closes the gap that OWL leaves open. OWL reasons under the open-world assumption, so a missing value is unknown rather than wrong: asserting that every Person has a birthDate does not make a Person without one an error, it makes the reasoner conclude the birthDate exists and has not been recorded.

Data pipelines need the opposite. When a crawler produces ten thousand Person nodes, the question is which are missing a name, carry a date in the wrong format, or point at an Organization where a Place was expected. SHACL evaluates each targeted node against explicit constraints, closed-world, and reports what is wrong and where. OWL derives facts; SHACL finds faults.

Shapes and targets

A shapes graph is RDF, written with the sh: prefix for http://www.w3.org/ns/shacl#. A sh:NodeShape constrains a node; a sh:PropertyShape constrains the values reachable along a path and attaches with sh:property. Targets say which nodes a shape applies to:

ex:PersonShape a sh:NodeShape ;
    sh:targetClass       ex:Person ;        # every instance of a class
    sh:targetNode        ex:MarieCurie ;    # one named node
    sh:targetSubjectsOf  ex:birthPlace ;    # anything used as a subject of a property
    sh:targetObjectsOf   ex:birthPlace .    # anything used as a value of a property

sh:targetClass is by far the most used; sh:targetObjectsOf checks values without knowing their type, for example that everything used as a birthPlace has a label. Inside a property shape, sh:path names the property and accepts path expressions such as sh:inversePath.

Constraint components

Each constraint is one triple in a shape.

Cardinality. How many values the path may have.

sh:minCount 1 ; sh:maxCount 1 ;

Value type. Datatype, class of an IRI value, or kind of RDF term.

sh:datatype xsd:date ;
sh:class    ex:Place ;
sh:nodeKind sh:IRI ;          # or sh:Literal, sh:BlankNode, sh:BlankNodeOrIRI, sh:IRIOrLiteral

Value range. Numeric and date bounds.

sh:minInclusive 0 ; sh:maxInclusive 130 ;
sh:minExclusive 0 ; sh:maxExclusive 100 ;

String. Length, pattern, language tags.

sh:minLength 1 ; sh:maxLength 200 ;
sh:pattern "^[A-Z][a-z]+$" ; sh:flags "i" ;
sh:languageIn ( "en" "fr" ) ; sh:uniqueLang true ;

Property pairs. Compare two properties on the same node.

sh:equals   ex:legalName ;    # same value set as this property
sh:disjoint ex:deathPlace ;   # no value in common
sh:lessThan ex:deathDate ;    # every value less than every value of that property

Logical. Combine shapes.

sh:not  ex:CompanyShape ;
sh:and  ( ex:PersonShape ex:EmployeeShape ) ;
sh:or   ( [ sh:datatype xsd:date ] [ sh:datatype xsd:gYear ] ) ;
sh:xone ( ex:PersonShape ex:OrganizationShape ) ;   # exactly one

sh:or is how to allow a property to hold either of two data types.

Shape-based. Require values to conform to another shape.

sh:node ex:PlaceShape ;
sh:qualifiedValueShape ex:PrimaryAddressShape ;
sh:qualifiedMinCount 1 ;

sh:node demands that every value conform; sh:qualifiedValueShape with sh:qualifiedMinCount or sh:qualifiedMaxCount demands that a given number conform, saying “at least one address must be a billing address” without constraining the rest.

Other. Enumerations, required values, closure.

sh:in ( ex:Male ex:Female ex:Other ) ;
sh:hasValue ex:Published ;
sh:closed true ; sh:ignoredProperties ( rdf:type rdfs:label ) ;

sh:closed true rejects any property no property shape mentions: the strongest guard against typos in generated data, and the one constraint with no OWL counterpart.

Severity and messages

Every constraint carries a severity, defaulting to sh:Violation, with sh:Warning and sh:Info as alternatives. Severity changes not whether a result is reported but how a pipeline should treat it. sh:message supplies text, optionally language-tagged, that appears in the report instead of a generic description.

sh:path ex:birthDate ;
sh:datatype xsd:date ;
sh:severity sh:Warning ;
sh:message "birthDate should be an xsd:date such as 1867-11-07."@en ;

A message on every constraint is what makes reports readable to people who did not write them.

A worked example

A Person shape: a required, single-valued string name; an optional xsd:date birthDate; a required birthPlace whose value must be a Place.

@prefix sh:   <http://www.w3.org/ns/shacl#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .
@prefix ex:   <https://example.org/> .

ex:PersonShape a sh:NodeShape ;
    sh:targetClass ex:Person ;

    sh:property [
        sh:path     ex:name ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message  "A Person needs exactly one name, as a string."@en ;
    ] ;

    sh:property [
        sh:path     ex:birthDate ;
        sh:datatype xsd:date ;
        sh:maxCount 1 ;
        sh:message  "birthDate must be a single xsd:date."@en ;
    ] ;

    sh:property [
        sh:path     ex:birthPlace ;
        sh:class    ex:Place ;
        sh:minCount 1 ;
        sh:message  "A Person needs a birthPlace, and it must be a Place."@en ;
    ] .

A data graph with one conforming node and one failing node:

@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex:  <https://example.org/> .

ex:Warsaw a ex:Place .
ex:Sorbonne a ex:Organization .

ex:MarieCurie a ex:Person ;
    ex:name       "Marie Curie" ;
    ex:birthDate  "1867-11-07"^^xsd:date ;
    ex:birthPlace ex:Warsaw .

ex:PierreCurie a ex:Person ;
    ex:name       "Pierre Curie" ;
    ex:birthDate  "1859-05-15" ;
    ex:birthPlace ex:Sorbonne .

Marie Curie conforms. Pierre Curie fails twice: his birthDate literal has no datatype, so it is an xsd:string, and the Sorbonne is an Organization, so it fails sh:class ex:Place. The processor returns a report as RDF:

@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix ex:  <https://example.org/> .

[] a sh:ValidationReport ;
   sh:conforms false ;
   sh:result [
       a sh:ValidationResult ;
       sh:focusNode                  ex:PierreCurie ;
       sh:resultPath                 ex:birthDate ;
       sh:value                      "1859-05-15" ;
       sh:sourceConstraintComponent  sh:DatatypeConstraintComponent ;
       sh:sourceShape                ex:PersonShape-birthDate ;
       sh:resultSeverity             sh:Violation ;
       sh:resultMessage              "birthDate must be a single xsd:date."@en ;
   ] ,
   [
       a sh:ValidationResult ;
       sh:focusNode                  ex:PierreCurie ;
       sh:resultPath                 ex:birthPlace ;
       sh:value                      ex:Sorbonne ;
       sh:sourceConstraintComponent  sh:ClassConstraintComponent ;
       sh:sourceShape                ex:PersonShape-birthPlace ;
       sh:resultSeverity             sh:Violation ;
       sh:resultMessage              "A Person needs a birthPlace, and it must be a Place."@en ;
   ] .

The report is itself a graph, so you can query it with SPARQL, store it, and diff it between runs. Each result names the focus node, path, offending value, and the constraint component that rejected it.

SHACL-SPARQL and Advanced Features

Some rules need more than the core components: aggregate conditions, cross-node comparisons, anything needing a join. SHACL-SPARQL, part of the same Recommendation, lets a constraint be a SELECT query whose bindings are the violations.

ex:NoFutureBirthShape a sh:NodeShape ;
    sh:targetClass ex:Person ;
    sh:sparql [
        sh:message "birthDate is in the future."@en ;
        sh:prefixes ex: ;
        sh:select """
            SELECT $this ?value WHERE {
              $this ex:birthDate ?value .
              FILTER (?value > NOW())
            }
        """ ;
    ] .

$this is bound to the focus node and must be projected; ?value populates sh:value in the report. Such a query can be wrapped in a reusable constraint component so shape authors never see the SPARQL.

SHACL Advanced Features is a separate W3C Note, not a Recommendation. It adds rules (triple and SPARQL rules that infer new triples, giving SHACL forward-chaining inference alongside validation) and node expressions. Support varies by implementation.

SHACL vs ShEx

ShEx (Shape Expressions) solves the same problem differently. It came out of the Wikidata and life-sciences communities and uses a compact, grammar-based syntax modeled on regular expressions: shapes are short to read but a separate artifact from RDF. SHACL shapes are RDF triples, generated with the same tools as the data.

SHACLShEx
StatusW3C Recommendation, 2017Community specification
Shapes written asRDF (Turtle, JSON-LD)Compact syntax ShExC, plus JSON and RDF forms
StyleConstraint components on targetsGrammar over triple expressions
Escape hatchSHACL-SPARQLShEx semantic actions
Typical useEnterprise pipelines, triple store validation, CIWikidata EntitySchemas, biomedical data exchange

The practical rule: SHACL where a store, a build pipeline or a vendor product runs the validation, since support is broader; ShEx where shapes are hand-written and read often, particularly around Wikidata, whose EntitySchemas use it.

Tooling

pySHACL, built on rdflib, is the usual choice for scripts and CI. Apache Jena includes a SHACL engine with a command-line validator; the TopBraid SHACL API is the Java implementation from the specification’s editors; and RDF4J supports SHACL. GraphDB and Stardog validate in the store, so a shapes graph can reject a transaction at write time.

from pyshacl import validate
from rdflib import Graph

data = Graph().parse("people.ttl", format="turtle")
shapes = Graph().parse("shapes.ttl", format="turtle")

conforms, report_graph, report_text = validate(
    data, shacl_graph=shapes, inference="rdfs", advanced=True
)

print(conforms)
print(report_text)

The inference argument runs RDFS or OWL-RL entailment first, which matters when a shape targets a class nodes belong to only by inference.

Where shapes fit in a pipeline

On ingest. Validate each batch before writing. In a store with native SHACL support, reject the transaction outright; otherwise, validate a staging graph and promote only what conforms. This is the only placement that keeps bad data out.

In continuous integration. Keep the shapes graph in version control next to the ontology and run pySHACL over fixture data on every commit, so a mapping change that would break the graph fails the build.

After extraction. Automated extraction produces plausible-looking triples with wrong types, empty labels, and dates in a dozen formats. Shapes over that output are the cheapest quality gate available, and the report becomes the review queue (/concepts/entity-linking-ner-and-extraction/).

For developers

Start with cardinality and datatype constraints on the properties downstream code depends on, and add more as real failures appear. A shapes graph written from the ontology upfront encodes aspirations rather than requirements, and every constraint that fires on legitimate data trains people to ignore the report.

Use severity deliberately: sh:Violation for anything that would break a consumer, sh:Warning for quality issues worth fixing, sh:Info for observations. Pipelines then block on violations and log the rest.

Cost scales with targeted nodes times constraints, and sh:closed and shape-based constraints are the expensive ones, so validate incrementally with sh:targetNode on changed nodes. SHACL sees only the graph it’s handed, so a node typed by an unmaterialized entailment is never targeted: turn on RDFS inference when shapes target inferred classes.

For SEOs

What SHACL does is familiar from structured data validators. When the Rich Results Test reports that an Article is missing a required headline, that is closed-world shape checking against Google’s requirements for a rich result, expressed as a documented property list rather than a shapes graph (/seo/structured-data-testing-tools/).

Shapes apply the same discipline to a site’s own rules. Where JSON-LD comes from templates or a CMS, one bad template silently breaks thousands of pages, and a validator that checks one URL at a time will not find it. Converting that JSON-LD to RDF and validating it against a shapes graph of house rules (every Organization carries a sameAs to Wikidata, every Article has an author resolving to a Person node) turns markup quality into a build-time check. See /ecosystems/structured-data/ and /languages/json-ld/.

Related pages

FAQ

What is SHACL used for?

SHACL validates RDF data against shapes describing what conforming nodes must look like. It checks that required properties are present, that values have the right datatype or class, that counts and ranges hold, and that no unexpected properties appear. A processor returns an RDF report naming every failing node, path, value and constraint.

What is the difference between SHACL and OWL?

OWL describes what is logically true and infers new facts under the open-world assumption, so a missing value is unknown rather than invalid. SHACL checks whether data conforms to expected shapes under a closed world and reports faults. An OWL cardinality axiom generates inferences; a SHACL sh:minCount generates a violation.

What is the difference between SHACL and ShEx?

Both validate RDF against shapes. SHACL is a 2017 W3C Recommendation whose shapes are themselves RDF, so they are stored and queried with the same tools as the data, and triple-store support is wider. ShEx uses a compact grammar-based syntax that is shorter to read and is the basis of Wikidata EntitySchemas.

How do you run SHACL validation?

Load the data graph and the shapes graph into a processor and compare them. In Python, pySHACL takes two rdflib graphs and returns a boolean, a report graph, and a text summary. Apache Jena provides a command-line validator, and GraphDB and Stardog validate at write time, rejecting a non-conforming transaction.

Sources and further reading

  • W3C, Shapes Constraint Language (SHACL): https://www.w3.org/TR/shacl/
  • W3C, SHACL Advanced Features: https://www.w3.org/TR/shacl-af/
  • W3C, Validating RDF Data (free book): https://book.validatingrdf.com/
  • Shape Expressions (ShEx) primer: https://shex.io/shex-primer/
  • pySHACL: https://github.com/RDFLib/pySHACL
  • Apache Jena SHACL: https://jena.apache.org/documentation/shacl/