SHACL Validation
Kastor supports SHACL validation with full SHACL 1.2 support, including Core features and SPARQL Extensions. Validation is available in two complementary ways:
- Kastor Gen ValidationContext for domain materialization and
RdfHandlevalidation. - Repository-level SHACL validation via the
rdf/shacl/validationmodule.
For the provider model (native Kastor engine vs optional adapters to Jena, RDF4J, and others), module layout, and implementation roadmap, see SHACL validation architecture. For performance benchmarking (JMH harness, ERA-SHACL-Benchmark CLI, baselines), see SHACL native engine: cross-implementation performance benchmarks.
Kastor Gen ValidationContext
Validation is explicit and optional. You decide when validation is enforced by passing a ValidationContext during materialization.
Add a Validation Adapter
Pick the adapter that matches your backend:
dependencies {
runtimeOnly(project(":kastor-gen:validation-jena"))
// or
runtimeOnly(project(":kastor-gen:validation-rdf4j"))
}
Validate During Materialization
val validation = JenaValidation()
val person: Person = rdfRef.asValidatedType(validation)
Validate After Materialization
val validation = JenaValidation()
val person: Person = rdfRef.asType(validation)
person.asRdf().validateOrThrow()
Repository-Level SHACL Validation
Module rdf/shacl/validation: API reference, providerId table, maxCombinedGraphTriples / rdf4jUntrustedInputLimits, and cross-engine smoke tests live in the module README.
Use the rdf/shacl/validation module when you want to validate graphs directly (without materialization):
import com.geoknoesis.kastor.rdf.*
import com.geoknoesis.kastor.rdf.shacl.ShaclValidation
// Create shapes using the Kotlin SHACL DSL (recommended; add dependency `com.geoknoesis.kastor:rdf-shacl-dsl`)
val shapesGraph = shacl {
nodeShape("http://example.org/PersonShape") {
targetClass(FOAF.Person)
property(FOAF.name) {
minCount = 1
}
}
}
// Or create shapes manually
val shapesGraph = Rdf.graph {
// ... manual RDF triples
}
// Validate
val validator = ShaclValidation.validator(ValidationProfile.SHACL_CORE)
val report = validator.validate(dataGraph, shapesGraph)
if (!report.isValid) {
report.violations.forEach { println(it.message) }
}
Conformance semantics
report.isValidmirrors SHACLsh:conforms. By default it isfalseif any validation result of severitysh:Violation,sh:Warning,sh:Infoor a custom severity exists; SHACL 1.2sh:Debug/sh:Traceresults do not affect conformance.ValidationConfig.conformanceDisallows: Set<Iri>?is SHACL 1.2sh:conformanceDisallows. It is the set of result severities (SHACL.Violation,SHACL.Warning,SHACL.Info,SHACL.Debug,SHACL.Traceor a custom severity IRI) whose results makeisValidfalse.null(the default) applies the SHACL default above. Results of other severities are still reported; they just do not block conformance. For example,ValidationConfig(conformanceDisallows = setOf(SHACL.Violation))lets warnings through. Results for undefined recursion and undecidablesh:targetWherecarry the source shape’s declared severity, so under that setting they still block for shapes of severitysh:Violation(the default). The native engine (kastor) honours this setting; the other providers do not read it.report.hasViolationsis the severity-filtered check (Violation/Error only). Use it when warnings should not fail a build. Whenreport.violationsTruncatedis true it fails closed and returnstrue: the results beyondmaxViolationsare not kept, so their severities are unknown.- Property shapes default to
sh:Violation; they do not inherit a node shape’s severity. - Deactivated shapes and shapes without constraints accept every node. Literal value nodes can conform to nested shapes (
sh:node,sh:and/sh:or/sh:xone/sh:not, qualified value shapes). - Repeatable parameters (
sh:pattern,sh:hasValue, bounds, …) produce one constraint per value. Repeating a single-valued parameter (sh:minCount,sh:flags,sh:in, …) is a shape compilation error.sh:patternis compiled once, at shape compile time, and supports the flagsi,m,s,xandq. sh:patternfollows XPathfn:matches(XML Schema regular expressions), notjava.util.regexdefaults.$matches only at the end of the value (withm: before a line feed), never before a final newline. Withm,^matches at the start and after a line feed only..matches everything except line feed and carriage return unlesssis set.\dis any Unicode decimal digit and\sis exactly space, tab, line feed and carriage return.\wis[_\p{L}\p{M}\p{N}\p{S}]: the XML Schema class (every character except punctuation, separators and control/unassigned characters, so^\w+$acceptsJoséand symbols such as+are word characters) plus the underscore. XML Schema excludes_; it is accepted because shapes are usually written against engines that evaluate patterns withjava.util.regex(Jena, RDF4J, TopBraid), where\wmatches_.\Wis the exact complement. The SHACL DSL guide lists the remaining differences from Java regular expressions.- The pattern syntax follows one rule. XML Schema syntax is translated: character-class subtraction (
[a-z-[aeiou]], which must end its class and may nest),\i/\c(the XMLNameStartChar/NameCharproductions) and their complements\I/\C, and block escapes\p{IsBasicLatin}(the Unicode block name without spaces). Patterns run on RE2/J, a linear-time engine (the same as the sandboxed SPARQL of both providers), so no pattern can backtrack catastrophically.java.util.regexsyntax that means the same is translated: lazy quantifiers, non-capturing and named groups,\Q…\E,\A,\z,\xHH. What the linear-time engine cannot evaluate is rejected at compile time: back-references (XPath has them), lookarounds, atomic groups, possessive quantifiers, word boundaries\b, repetition counts above 1000 (a count may have leading zeros:{04}is{4}), groups nested more than 64 deep, and patterns too complex to compile (longer than 65,536 characters, or whose translation passes a structural parse cost of about eight\p{L}-sized classes or 16,000 literal characters: RE2/J parses in quadratic time, so the cost is bounded, as a count, before it parses). Three common idioms of other dialects are translated with their intended meaning: a leading inline flag group ofi,m,s,x((?i)^abc$is^abc$withsh:flags "i"; the group’s flags are merged withsh:flags), a script name in\p{IsX}whenXis not a block name (\p{IsLatin}), and the twelve POSIX bracket expressions inside a class ([[:alpha:]_], US-ASCII). Everything else that would mean something else is rejected when the shapes are compiled (ShapeCompileException): class intersection&&, an unescaped[inside a class (nested classes and unions), inline flag groups that are not at the start, are scoped or negated, or have other flags (a(?i)b,(?i:…),(?-i),(?u)), and\p{IsX}whereXis neither a block nor a script (Java binary properties such as\p{IsAlphabetic}). - Character classes are XML Schema classes. A class cannot be empty, and
]is a member only when escaped:[].]and[^]]are rejected (Java and POSIX read the]as a member).-is a literal at the start or end of a class, after a range and next to a class escape ([\w-.]); between two characters it makes a range, whose endpoints may be escapes ([\x41-\x5A]) but not an unescaped hyphen ([a--b]is rejected; write\-).-[always starts a subtraction. - The
iflag makes characters, character ranges (also in negated classes and subtractions) and back-references case-insensitive, and nothing else (XPath F&O 5.6.2; back-references are not supported):\p{Lu}still matches upper-case letters only, also inside a class such as[\p{Lu}x].java.util.regexwould make\p{Lu}match every cased letter. - One pattern evaluation (one pattern against one value) has a budget,
ValidationConfig.patternTimeout(default 1 second). The budget is counted in steps of the regular expression engine, 200,000 per millisecond of the duration (two hundred million by default). Each character the engine reads costs as many steps as the compiled pattern has instructions (the most work the linear-time engine does per character), so the count measures work:.{0,1000}x, about 2,000 instructions, uses the default budget on about 100,000 characters,^[a-z]+$on tens of millions. Whether a value is decided therefore does not depend on the machine, its load or a garbage collection pause; the wall clock is only a backstop, at ten times the duration. Patterns run on a linear-time engine, so what uses up this budget is a very long value, or a long value of a pattern with a large program. A value on which a pattern uses up the budget does not abort the run. The constraint is undecided for that value: the report gets a result that names the pattern, has the shape’s severity andsh:PatternConstraintComponent, and is markedisPatternTimeout(ksh:resultStatus ksh:PatternTimeoutin RDF). The value is not accepted, so the report cannot conform because of it, and validation continues with the other values and focus nodes. A shape that reads the answer throughsh:not,sh:orand the like gets “undefined” (see three-valued conformance), so an undecided pattern never satisfies or silently fails an outer constraint. WithstrictMode = true, validation throwsShaclValidationExceptionnaming the pattern instead. Only the run-widetimeoutaborts a run. The budget is per value, so a run also has a cap: once 16 evaluations of the run have used up their whole budget, every value not matched before gets 1/1024 of it, but at least 1,024 steps and at least 64 steps per character of the value (never more than the full budget). The engine reads each character about three times, so a value of any length (base64, WKT…) is still decided for a pattern of up to about 20 instructions, a value of a larger pattern may be undecided at a cost linear in its length, and data that never uses up a budget never reaches the cap. Within a run a (pattern, value) has a single answer, whoever asks it (the report,sh:node,sh:not…); only whether a value that is expensive to match but not hostile is decided can depend on whether it is first matched before or after the cap is reached. - The linear-time engine does not recurse on its input, so a match cannot exhaust the stack;
isPatternTooComplex(ksh:PatternTooComplex) is no longer reported (the term stays in the vocabulary). - RDF lists in the shapes graph (
sh:in,sh:and/sh:or/sh:xone,sh:languageIn,sh:ignoredProperties, sequence and alternative paths) must be well-formed: a cell with a missing or repeatedrdf:first/rdf:restis a shape compilation error rather than a silently truncated list. List cells may be blank nodes or IRIs. A sequence path and an alternative path need at least two members:sh:path ( )andsh:path ( ex:p )are shape compilation errors. The shapes graph is read as a set of triples, so a graph implementation that returns the same triple twice does not make a parameter look repeated. - Classes that are also shapes act as implicit class targets. Value comparisons (
sh:lessThan, bounds, …) follow the SPARQL operator mapping for datatypes, and literals are checked for XSD lexical validity. - Numbers are compared after XPath numeric type promotion: two decimals (or integers) exactly; a decimal and an
xsd:floatas floats; anything with anxsd:doubleas doubles. So"0.1"^^xsd:floatsatisfiessh:maxInclusive 0.1(both are the float nearest to 0.1), while it is above"0.1"^^xsd:double. Negative zero equals zero, andNaNsatisfies no range or order constraint (it is not comparable, even to itself).sh:inandsh:hasValuecompare RDF terms, not values:"0.0"^^xsd:decimalis not insh:in ( 0 ). - An ill-formed parameter value is a shape compilation error, not a skipped constraint: a literal as
sh:classorsh:nodeKindvalue, for example. owl:importsof the shapes graph are followed only withImportConfig(resolveOwlImports = true), and only offline: an import is resolved whenDatasetValidationConfig.auxiliaryGraphshas a graph for its IRI. An import without a graph, or beyondmaxImportDepth, is skipped and listed inreport.warnings(ksh:warningin RDF), because the shapes it would contribute are not validated.
Three-valued conformance
The native engine answers every nested conformance check (sh:node, logical constraints, qualified value shapes, sh:someValue, sh:targetWhere, …) with conforms, fails or undefined, and combines the answers with Kleene logic:
- a shape and
sh:andfail as soon as one part definitely fails; sh:orandsh:someValueconform as soon as one part definitely conforms;- an answer is undefined only when it really depends on an undefined answer.
As a result, reports never depend on the order of operands, constraints or targets. When one value node is undefined, the definite violations of the other value nodes are still reported, and the undefined value adds an undefined result (see below). Qualified value counts use a lower bound (values that definitely conform) and an upper bound (values that conform or are undefined). A count is undefined only when the two bounds lead to different outcomes.
Undefined answers come from recursive shapes, described next, and from sh:pattern evaluations that could not be completed (see conformance semantics). A result has one status. When an answer is undefined for both reasons, the result is marked ksh:UndefinedRecursion and its message also names the pattern; a constraint with several undefined operands reports undefined recursion in preference to a pattern, whatever the order of the operands.
Recursive shapes
SHACL does not define recursive shapes. At compile time, the native engine finds the shapes that can reach themselves (the strongly connected components of the shape dependency graph), because only those can recurse over data. A shape depends on the shapes it references, on the shapes its node expressions validate against, and on the shapes of the bodies of the SHACL SPARQL functions its queries may call (found from the function IRIs each query mentions, then through the calls of those bodies). A computed shape (a shnex:conformsToShape shape argument or a sh:nodeByExpression value that is no IRI) whose possible outputs are constants, such as [ shnex:if ... ; shnex:then ex:A ; shnex:else ex:B ], depends on those shapes only. One read from the data, a variable or a query may name any shape, so it depends on every shape of the graph, and so does a query that calls a relative IRI without an absolute BASE. The compiler compiles exactly the shapes this analysis follows, plus those of sh:targetWhere and of sh:targetNode expressions. It then applies these rules.
Monotone recursion conforms (greatest fixpoint). A shape that depends on itself only through sh:node, sh:and, sh:property, sh:or, sh:someValue or sh:qualifiedMinCount is assumed to conform until one of its constraints fails. Valid cyclic data therefore conforms. sh:shape, sh:memberShape, sh:reifierShape and sh:nodeByExpression also count as monotone.
# Shapes: recursion through sh:or
ex:PersonShape a sh:NodeShape ; sh:targetClass ex:Person ;
sh:property [ sh:path ex:knows ; sh:or ( ex:NamedShape ex:PersonShape ) ] .
ex:NamedShape sh:property [ sh:path ex:name ; sh:minCount 1 ] .
# Data: a cycle. Conforms, also with strictMode = true.
ex:a a ex:Person ; ex:knows ex:b .
ex:b a ex:Person ; ex:knows ex:a .
A cycle can still fail. If a node on it definitely breaks a constraint (for example, a sh:minCount on ex:knows at the end of a chain), the nodes that depend on it get ordinary sh:Violation results.
Recursion through non-monotone operators is undefined. Consider a dependency cycle that passes through sh:not, sh:xone, sh:qualifiedMaxCount or the sibling exclusion of sh:qualifiedValueShapesDisjoint true. It has no defined answer. Each constraint whose outcome depends on that answer produces a result stating that the recursive dependency is undefined. The result has isUndefinedRecursion == true (violation code ValidationViolation.UNDEFINED_RECURSION_CODE) and keeps the constraint’s component and the source shape’s declared severity, so it affects report.isValid exactly as a failure of that constraint would, whatever conformanceDisallows is. The report therefore conforms only when it conforms whatever the undefined answers are. With ValidationConfig(strictMode = true), validation throws ShaclValidationException instead. If a non-recursive part of the shape already fails, that failure decides the outcome and no undefined result is produced.
# Shapes: ex:S depends on itself through sh:not
ex:S a sh:NodeShape ; sh:targetNode ex:x ;
sh:property [ sh:path ex:name ; sh:minCount 1 ] ;
sh:property [ sh:path ex:self ; sh:not ex:S ] .
# Data: one blocking sh:Violation result marked isUndefinedRecursion ("... is undefined"); strictMode throws.
ex:x ex:self ex:x ; ex:name "x" .
Deep data is stack-safe. Recursive shapes are solved with an explicit worklist, not with JVM stack proportional to the data. For example, the following 10,000-node chain validates. A missing name at its tail is reported for every node of the chain.
val chain = Rdf.graph {
for (i in 0 until 10_000) {
val p = iri("http://example.org/p$i")
p - RDF.type - iri("http://example.org/Person")
p - iri("http://example.org/name") - string("person $i")
if (i + 1 < 10_000) p - iri("http://example.org/knows") - iri("http://example.org/p${i + 1}")
}
}
// PersonShape: sh:property [ sh:path ex:knows ; sh:node ex:PersonShape ] (plus a name constraint)
val report = ShaclValidation.validator().validate(chain, shapesGraph)
maxRecursionDepth limits non-recursive nesting only. ValidationConfig.maxRecursionDepth (default 64) limits how deeply shapes that are not recursive can nest through sh:node, logical constraints and similar references. Recursive shapes are solved as described above and are not bound by it. Exceeding the limit throws ShaclValidationException.
val validator = ShaclValidation.validator(ValidationConfig(maxRecursionDepth = 128))
sh:targetWhere
The native engine checks every node of the data graph against the membership shape of sh:targetWhere. It first prunes candidates using the membership node shape’s own constraints:
- with
sh:class, only instances of that class are checked; - with
sh:nodeKind, the node kinds it excludes (literals or non-literals) are skipped; - with
sh:datatype, only literals are checked.
When a candidate’s membership is undefined (recursion through a non-monotone operator), the report gets an undefined result (isUndefinedRecursion) for that candidate with the target shape’s severity. It blocks conformance whenever a failure of that shape would. With strictMode = true, validation throws ShaclValidationException. SPARQL node expressions used as sh:targetWhere values are not supported; see Unsupported features.
Report contents
ValidationViolation carries the full result path (resultPathNode, plus resultPathTriples for blank-node paths), the shape’s sh:message values with language tags (resultMessages) and sourceConstraint (for example, the SHACL-SPARQL constraint node). toShaclValidationReportRdf() emits sh:resultPath, sh:resultMessage and sh:sourceConstraint, emits sh:value only for components that define it, and emits one sh:closed result per offending triple. A blank-node sh:resultPath is copied with fresh blank nodes per result. A blank node the path uses twice (( _:inv _:inv )) is unfolded per use, as the W3C suite expects, while the unfolded copy has at most 1,024 nodes; a larger one (blank nodes shared level after level) shares one copy per source blank node instead, so the copy is never larger than the source structure.
Property path limits. A blank node that a path uses several times is parsed once and shared. A path whose unfolding (the tree that evaluation, $PATH rendering and comparison walk, counting a shared sub-path once per use) has more than 4,096 nodes, or that nests more than 128 constructors deep, is ill-formed for the engine: compilation fails with ShapeCompileException. Hand-written paths have a few dozen nodes; the limit stops a few shared levels from unfolding exponentially.
What the SHACL report vocabulary cannot express is exported with Kastor extension terms (KastorShaclVocabulary, prefix ksh:, namespace https://kastor.geoknoesis.com/ns/shacl#). They are additional triples on the sh:ValidationReport and its sh:ValidationResult nodes; nothing is minted in the sh: namespace, and consumers of the standard vocabulary can ignore them.
| Term | Meaning |
|---|---|
ksh:resultStatus |
On a result: the constraint was not decided, which is not the same as a failure (violation.isUndecided, violation.resultStatus). The result has the severity and constraint component a failure would have, so without this triple an RDF consumer could not tell the two apart. Its value is one of the three statuses below. |
ksh:UndefinedRecursion |
The constraint depends on a recursive shape dependency through a non-monotone operator (isUndefinedRecursion). |
ksh:PatternTimeout |
A sh:pattern evaluation used up its budget (ValidationConfig.patternTimeout, counted in regular expression engine steps) on a value (isPatternTimeout). |
ksh:PatternTooComplex |
The regular expression engine ran out of stack while matching a sh:pattern (isPatternTooComplex). No longer reported: patterns run on a linear-time engine. |
ksh:reifier |
On a sh:reifierShape result: the reifier that does not conform, or whose conformance is undecided. sh:value is the object of the reified triple, so several reifiers of one triple differ only by this property (and by the engine message). In Kotlin it is violation.context[ValidationViolation.REIFIER_CONTEXT_KEY]. |
ksh:warning |
On the report: the message of a report-level warning (report.warnings entries without a resource), for example a construct skipped under IGNORE_WITH_WARNING. Warnings do not affect sh:conforms; this property keeps a conforming RDF report from hiding what was not validated. |
The vocabulary is published as a Turtle document inside the rdf-shacl-validation JAR, at the classpath resource KastorShaclVocabulary.VOCABULARY_RESOURCE (/com/geoknoesis/kastor/rdf/shacl/kastor-shacl.ttl). Every term has an rdfs:label, an rdfs:comment and rdfs:isDefinedBy; ksh:ResultStatus is the class of the three statuses. The document has an owl:versionInfo (KastorShaclVocabulary.VERSION); the namespace IRI is stable and carries no version, and terms are only ever added. KastorShaclVocabulary.terms lists the terms, and a test checks that every ksh: term the engine emits is declared in the document.
val vocabulary = KastorShaclVocabulary::class.java.getResourceAsStream(KastorShaclVocabulary.VOCABULARY_RESOURCE)!!
.use { Rdf.parse(it.readBytes().decodeToString(), RdfFormat.TURTLE) }
Each failing reifier yields its own result; results that would be identical are reported once. sh:reificationRequired is a parameter of sh:ReifierShapeConstraintComponent, which is the component reported for both ConstraintType.REIFIER_SHAPE and ConstraintType.REIFICATION_REQUIRED results.
SHACL-SPARQL
sh:sparql constraints need a SPARQL engine at runtime: add rdf-jena or rdf-rdf4j. Without one, validation fails with a ShaclValidationException naming those modules, and the native provider’s capability flags report SHACL-SPARQL as unsupported. Queries:
- run over the data graph (see where queries run);
- honour
sh:prefixes/sh:declare,$PATH,sh:deactivatedand message templates; - pre-bind
$this(as in SHACL 1.2,$currentShapeand$shapesGraphare not pre-bound: they are ordinary variables); - produce one result per solution row, with
sh:sourceConstraint.
A sh:sparql constraint may set sh:severity, which becomes the severity of its results, and sh:resultAnnotation (sh:annotationProperty, sh:annotationVarName, sh:annotationValue) to add annotation triples to them.
Prefix declarations
sh:prefixes / sh:declare are followed through owl:imports, including owl:versionIRI navigation. A node with no sh:prefixes uses the graph-level declarations of the SHACL instances of owl:Ontology, sh:DataGraph, sh:ShapesGraph and sh:RulesGraph. Conflicting declarations, declarations with the wrong datatype, and an sh:prefix that is not a SPARQL prefix name (PN_PREFIX: empty, or a letter followed by letters, digits, _, - or ., not ending in .) make the shapes graph ill-formed. Kastor writes each declaration into the query prologue, so this check stops a prefix from injecting query text.
SPARQL-based constraint components
A sh:ConstraintComponent with sh:parameter (and sh:optional) is evaluated like a built-in component:
- Validators: SELECT validators come from
sh:nodeValidator/sh:propertyValidator, ASK validators fromsh:validator. The validator is chosen as in spec section 4.2.3; a component with no validator that fits the shape kind is ignored. - Parameters: parameter values are pre-bound. A shape with several values for a parameter yields one constraint per combination, up to 1024 combinations per shape and component; more than that makes the shapes graph ill-formed. A parameter whose declaration rules out blank nodes can also be computed by a node expression for each focus node: see Dynamic SHACL.
- Results: results report
sh:sourceConstraintComponentas the component IRI. The message comes from the validator, else the component, else the shape, with{$param}substitution.
Node expressions
SHACL 1.2 node expressions (Node Expressions spec) are supported in full:
- The
shnex:library: constants, empty expressions (an empty blank node[]), list expressions (an RDF list),shnex:var,shnex:pathValues,shnex:exists,shnex:if,shnex:distinct,shnex:intersection,shnex:concat,shnex:remove,shnex:filterShape,shnex:limit,shnex:offset,shnex:orderBy,shnex:flatMap,shnex:findFirst,shnex:matchAll,shnex:count,shnex:min,shnex:max,shnex:sum,shnex:instancesOf,shnex:nodesMatchingandshnex:conformsToShape. sparql:<name>functions (spec section 5), custom functions with named parameters or list parameters (shnex:arg), and the SPARQL expressionssh:select/sh:sparqlExpr. As spec Appendix A requires, a custom function used in a node expression must be a SHACL instance ofsh:NamedParameterExpressionFunctionand a SHACL subclass ofsh:NamedParameterExpression(or ofsh:ListParameterExpressionFunctionandsh:ListParameterExpression), for exampleex:f a sh:ListParameterExpressionFunction ; rdfs:subClassOf sh:ListParameterExpression. Without the subclass, a call of the function is no node expression, which is reported asNODE_EXPRESSION. A function that is only called from SPARQL needs just the type.
They are accepted at sh:values, sh:defaultValue and sh:targetNode, and by two constraint components: sh:expression (sh:ExpressionConstraintComponent) and a computed sh:nodeByExpression. They also compute constraint parameters (see Dynamic SHACL).
- Positions: at
sh:targetNode,$thisis bound to the shape node; atsh:valuesandsh:defaultValueit is bound to the focus node. Atsh:targetNode, list and empty expressions still follow the data-node rule (see “Blank nodesh:targetNodevalues” below). - Values: the value nodes of a property shape are the path values plus the
sh:valuesoutputs, andsh:defaultValueapplies when that set is empty. Both are allowed only on predicate paths. Constant IRI and literal values work too. An IRI node that carriessh:select/sh:sparqlExprand is used there is ill-formed. sh:expression: it is evaluated with the shape’s focus node and the scope{value: v}(spec section 7.1).shnex:if,shnex:matchAllandsh:expressioncompare against the term"true"^^xsd:boolean, so"1"^^xsd:booleandoes not count as true.- Failures: an evaluation failure during validation fails validation with
ShaclValidationException. An ill-formed expression is aShapeCompileException. - Term equality:
shnex:distinct,shnex:intersectionandshnex:removecompare terms exactly, so"01"and"1"are different. sparql:functions: each call is one query evaluated by the SPARQL provider, so SPARQL error and unbound handling come from the provider. An argument that fails or yields several outputs is a SPARQL error, injected as(1/0);sparql:boundof a failing argument is a failure. The one exception is an argument that failed on a conformance check the engine could not decide: it fails the call instead of being a SPARQL error (see undecided checks fail closed).sparql:encodeisENCODE_FOR_URIand takes exactly one argument (the two-argument example of the spec table is not accepted).shnex:sumalso uses the provider, so it may trigger the private data copy (see where queries run).sh:deactivated: a shape’ssh:deactivatedmay be any node expression (SHACL 1.2 Core section 3.1.6). It is evaluated for each focus node, with that focus node and an empty scope, and the shape is deactivated for that focus node when the expression produces exactlytrue(the term"true"^^xsd:boolean). Any other output, including no output, leaves the shape active. An evaluation failure fails validation. The shapes the expression reads are dependencies of the shape, like the shapes ofsh:expression. While the recursion solver has not yet settled an answer the expression reads, the shape’s constraints are evaluated all the same: if they hold, the shape conforms for that focus node whatever the expression turns out to be (“deactivated, or the constraints hold” is a Kleene disjunction); otherwise it is undefined until the solver settles the answer. The same holds for thesh:deactivatedof ansh:sparqlconstraint and of a reifier. A shape that is deactivated for a node conforms for it everywhere: in nested checks, and as ansh:targetWheremembership shape, whose candidates are then not narrowed by itssh:class,sh:nodeKindorsh:datatype. A focus node for which the shape is deactivated gets nosh:uniqueValuesForresult, but it is still one of the targets whose keys the others are compared with. Thesh:deactivatedof ansh:sparqlconstraint may be a node expression too (SHACL 1.2 SPARQL): it is evaluated in the same way, per focus node, and deactivates only that constraint. One that is no node expression is reported asNODE_EXPRESSION. A shape or SPARQL constraint has at most onesh:deactivatedvalue:sh:deactivated true , [ ... ]is a compile error.- Deactivating one constraint with a reifier: a reifier of a triple whose subject is a shape and whose predicate is a parameter (
ex:P sh:minCount 1 {| sh:deactivated true |}) deactivates the constraints that use the triple (SHACL 1.2 Core section 3.1.6), for every constraint kind: the core components,sh:property,sh:node,sh:and/sh:or/sh:xone/sh:not,sh:closed,sh:uniqueValuesFor,sh:expression,sh:nodeByExpression,sh:reifierShape,sh:sparqland the parameters of SPARQL-based constraint components. The value may be the constanttrueor a node expression, evaluated like a shape’s: per focus node of the shape (for a property shape, the focus node, not a value node), with an empty scope, deactivating only for exactlytrue; a failure fails validation, and the shapes it reads are dependencies of the shape (negative ones, so a paradox is undefined recursion). A constraint that uses several parameter triples is deactivated by a reifier of any of them:sh:flagsdeactivates everysh:patternof the shape,sh:ignoredPropertiesdeactivatessh:closed,sh:qualifiedValueShapeandsh:qualifiedValueShapesDisjointdeactivate both qualified cardinality components,sh:reificationRequiredthesh:reifierShapeconstraints, while a reifier ofsh:qualifiedMinCount(orsh:qualifiedMaxCount) deactivates only that component. A constraint deactivated per focus node is not used to narrowsh:targetWherecandidates, and a focus node it is deactivated for gets nosh:uniqueValuesForresult. When a constraint uses several parameter triples whose reifiers carry node expressions, one of them producingtruedeactivates it even if another reads a recursive answer that is still undecided (Kleene disjunction). A parameter triple has at most one reifier withsh:deactivated(the syntax ruledeactivated-reifier-maxCount), checked on every triple of a constraint, even when another one already deactivates it with the constanttrue. That reifier may have at most onesh:deactivatedvalue: more is a compile error. This second rule is engine strictness. Section 3.1.6 speaks of “the value of sh:deactivated” in the singular, and the engine reads that as at most one value, as it does for a shape’ssh:deactivated. A reifier’ssh:deactivatedon a triple that is no constraint parameter (a target,sh:path,sh:message,sh:severity,sh:values, …) has no effect. The report carries a warning for it (sh:deactivated on a reifier of non-parameter triple ... has no effect), which is no unsupported feature and does not changeisValid. The shapes such an expression reads are not inspected for unsupported features, and neither is an expression there that is no node expression. - Shape-based expressions:
shnex:filterShape,shnex:findFirst,shnex:matchAll,shnex:nodesMatchingandshnex:conformsToShapeuse the engine’s conformance check. An undecided (final) conformance is a failure, and so is a validation error of the check (an ill-formed or unsupported candidate shape, a nested evaluation failure); no surrounding function absorbs these failures (see undecided checks fail closed). A shape read from a node expression is treated as non-monotone, so a data cycle through such a read is undecided and fails validation. The exception is ansh:expressionthat is ashnex:conformsToShapewith a constant shape, or ashnex:matchAll, whose other argument reads no shape and runs no query: it is monotone, so its reads count like those ofsh:node, and a data cycle through them conforms unless a constraint on it fails. A computed shape IRI may name any shape of the shapes graph, including a deactivated one (every node conforms to it). Inside the recursion solver,shnex:matchAllfollows Kleene logic: one node that definitely does not conform makes it false even while another node’s answer is not settled yet.shnex:filterShapeandshnex:nodesMatchingcheck every node before they wait for an unsettled answer, so a failure of a later check wins. Unrelated malformed shapes do not fail the run unless they are used.
Limits:
- Expression nesting is capped at 128 levels.
- Calls of SPARQL-callable functions (
sh:ListParameterExpressionFunction, from SPARQL or from node expressions) are capped at 16 under way at a time. Native custom function calls are bounded by the expression nesting cap of 128. - Every intermediate list is capped at
ValidationConfig.maxPathValueNodes. - Every evaluation step ticks the run budget, so the validation timeout bounds everything.
Undecided checks fail closed
This is a deliberate deviation from SHACL 1.2 Node Expressions sections 3.3 and 5. A failing argument of a SPARQL function expression is a SPARQL error (section 3.3), and sparql:coalesce skips arguments in error (section 5), so by the text [ sparql:coalesce ( [ shnex:conformsToShape ( ... ex:CheckShape ) ] true ) ] is true even when validating against ex:CheckShape failed or could not be decided. Kastor does not absorb such failures. A failure of a shape-reading expression stops validation with ShaclValidationException, whatever surrounds it. The shape-reading expressions are shnex:conformsToShape, shnex:filterShape, shnex:nodesMatching, shnex:findFirst, shnex:matchAll, and custom functions whose bodies run them. The failures covered are a final undefined answer (a paradox, a data cycle through a negative read, an undecided sh:pattern), an ill-formed or unsupported candidate shape, and a validation error while checking the node. The functions that would otherwise absorb them include sparql:coalesce, the unselected branch of sparql:if, sparql:logical-or / sparql:logical-and, sparql:bound, an argument of a custom function, and a SPARQL query (sh:select, sh:sparqlExpr, sh:sparql, SPARQL-based components) that calls a function body. This holds at every position that evaluates expressions: sh:values, sh:defaultValue, sh:targetNode, sh:expression, sh:nodeByExpression, sh:deactivated (of shapes, sh:sparql constraints and reifiers), Dynamic SHACL parameters and computed parameters of SPARQL-based components. Checks that are never evaluated cannot fail: the unselected branch of shnex:if, the nodes after the first match of shnex:findFirst, and the operand that a short-circuiting && or || inside a SPARQL query skips. In node expressions, by contrast, sparql:if and sparql:logical-* evaluate every argument.
The reason: such a failure means the engine does not know the answer. Absorbing it would turn “unknown” into a definite result. The data could then conform, or a shape be deactivated, because a check could not be made, and an undecided answer elsewhere in the engine always shows up as an undefined result or an error, never as a definite one. Inside SPARQL-called function bodies the engine already failed the calling query on such a failure; node expressions now behave the same way.
Not covered:
- A provisional “undefined” answer read while the recursion solver is still iterating is not a failure. The solver evaluates the question again, and only its final answer can fail.
- Ordinary evaluation errors stay SPARQL errors that
sparql:coalesceand the others may absorb, as section 5 says. Examples are[ sparql:coalesce ( [ sparql:abs ( ( 1 2 ) ) ] "ok" ) ](several output nodes), a type error, an unbound value, a SPARQL error of a query, and a function body that fails for one of these reasons. A shape argument that is not a shape of the shapes graph at all is such an ordinary error too.
The sh:sourceConstraint of an sh:expression result is the expression, and that of an sh:nodeByExpression result is the shape. When it is a blank node, toShaclValidationReportRdf() writes its deep copy (spec sections 7.1 and 7.2, SHACL 1.2 Core “Deep Copy”): its triples and those of the blank nodes they reach, RDF lists included, with blank node labels of the report’s own. Validation copies nothing: the results only carry a handle on the shapes graph (in ValidationViolation.context["sourceConstraintTriples"]), and the copy is made when toShaclValidationReportRdf() is called. The writer walks it one blank node at a time, and each blank node is written once per report, even when many results or many expressions share it: shared structure is written once, a cycle ends where it closes, and all the copies of a report together hold at most the shapes graph. The copy uses labels of its own, so a sh:focusNode or sh:value that is the same blank node as the source constraint (possible when the shapes graph is also the data graph) is not relabelled and no longer co-refers with the copy.
Dynamic SHACL
SHACL 1.2 Node Expressions section 8 (Dynamic SHACL) lets a node expression compute the value of a constraint parameter for each focus node. The native engine supports it, always: there is no ValidationConfig flag and no API change. A computed value is ill-formed without Dynamic SHACL, so supporting it changes the outcome for no well-formed shapes graph. A few ill-formed constants did change meaning: see the CHANGELOG.
ex:PresidentShape-age
a sh:PropertyShape ;
sh:path ex:age ;
sh:minInclusive [
shnex:if [ sparql:equals ( [ shnex:pathValues ex:country ] ex:USA ) ] ;
shnex:then 35 ;
shnex:else 18 ;
] .
ex:Address-state
a sh:PropertyShape ;
sh:path ex:state ;
sh:in [ shnex:pathValues ( ex:country ex:stateCode ) ] .
These are Examples 31 and 34 of the specification: the minimum age depends on the country of the president, and the allowed states on the country of the address.
Which parameters. A parameter is computed when its value is a blank node that is a node expression (and, where the parameter takes a SHACL list, not a list). The parameters fall in three groups, which decide how many output nodes are accepted:
| Group | Parameters | Output nodes |
|---|---|---|
| Single node | sh:minCount, sh:maxCount, sh:minLength, sh:maxLength, sh:minInclusive, sh:maxInclusive, sh:minExclusive, sh:maxExclusive, sh:uniqueLang, sh:singleLine, sh:minListLength, sh:maxListLength, sh:uniqueMembers, sh:pattern, sh:flags, sh:qualifiedMinCount, sh:qualifiedMaxCount, sh:qualifiedValueShapesDisjoint, sh:reificationRequired, sh:closed |
exactly one |
| IRI or list | sh:class, sh:datatype, sh:nodeKind, sh:rootClass, sh:uniqueValuesFor |
the members; at least one |
| List | sh:in, sh:languageIn, sh:ignoredProperties |
the members, in order; none is the empty list |
The parameters of SPARQL-based constraint components are computed under the rule further down.
Parameters that are not computed, and why.
| Parameters | Reason |
|---|---|
sh:node, sh:property, sh:qualifiedValueShape, sh:not, sh:and, sh:or, sh:xone, sh:memberShape, sh:someValue, sh:shape, sh:reifierShape |
the value is a shape, which may be a blank node |
sh:hasValue |
the value may be any node, a blank node included |
sh:path, sh:equals, sh:disjoint, sh:lessThan, sh:lessThanOrEquals, sh:subsetOf |
the value is a property path, which may be a blank node |
sh:sparql |
the value is a blank node |
sh:targetNode, sh:values, sh:defaultValue, sh:deactivated, sh:expression, sh:nodeByExpression |
already node expressions (see above) |
sh:severity, sh:message |
not constraint parameters |
Evaluation. The expression is evaluated in the data graph, with the focus node of the shape and an empty scope, so shnex:var "value" is unbound (unlike at sh:expression). On a property shape the focus node is the node the shape applies to, never a value node. A parameter is evaluated once for each focus node and constraint, before any value node is visited. The result is the ordinary constraint with the computed values, reported like a constant one: the same sh:sourceConstraintComponent, sh:value and messages (a default message quotes the computed value). A reifier of the parameter triple that deactivates the constraint is checked first, so a deactivated constraint never evaluates its expressions. Constant parameters are not affected, so every fast path stays. A computed sh:class, sh:nodeKind or sh:datatype does not narrow sh:targetWhere candidates.
Output rules. An output that breaks one of them fails validation with a ShaclValidationException (“Dynamic SHACL: …”), as any failed evaluation does.
- A single node parameter fails on no output and on two or more. The same holds for a parameter of a SPARQL-based constraint component, even an
sh:optionalone. - An IRI or list parameter fails on no output.
- Counts and lengths need a non-negative
xsd:integer,xsd:nonNegativeIntegerorxsd:longliteral that fits in a 32-bit signed integer (other integer datatypes, such asxsd:int, are rejected), bounds a literal, and booleans anxsd:booleanliteral. A computed"1"^^xsd:booleanor"0"^^xsd:booleanis well typed but not true, as everywhere else: only"true"^^xsd:booleanis true. sh:patternandsh:flagsneedxsd:stringliterals, and the pattern must compile with the flags.sh:class,sh:rootClass,sh:datatype,sh:uniqueValuesForandsh:ignoredPropertiesmembers need IRIs.sh:languageInmembers needxsd:stringliterals.sh:nodeKindtakes one of the seven node kinds when there is one output, and one of the four kinds a list may hold when there are several.sh:closedneeds anxsd:booleanliteral orsh:ByTypes:trueandsh:ByTypesclose the shape, any other boolean does not, and anything else fails.- A triple term needs
ValidationConfig.allowTripleTermsInShapeParameters, as for a constant. - The output is capped by
ValidationConfig.maxPathValueNodes, and every evaluation ticks the run budget.
Shapes, recursion and limits. The shapes a parameter expression reads are compiled, and the shape depends on them as it does for sh:expression. Unlike the monotone forms of sh:expression (see shape-based expressions), a parameter expression’s reads are always non-monotone, so a data cycle through one is undecided and fails validation. A computed sh:pattern is bounded by the pattern budget (patternTimeout). The engine keeps at most 4096 instantiated constraints per run, holding at most maxPathValueNodes nodes in all (their parameter values and the instances built from them): past either limit, more are built when needed and none is evicted.
Details by parameter.
sh:patternandsh:flags: with computedsh:flags, a constantsh:patternis checked at compile time only for being a string literal. Its regex is compiled at run time with the resolved flags, because validity can depend on them (q,x). With constantsh:flags, the flags are validated at compile time, also whensh:patternis computed.sh:closed: a computedsh:closedmust be the onlysh:closedvalue of the shape, and at most onesh:ignoredPropertiesvalue may be computed (its output is added to the constant values). Otherwise the shapes graph is ill-formed (ShapeCompileException). A computedsh:ignoredPropertiesis evaluated only for focus nodes for which the shape is closed. It is dropped at compile time whensh:closedis constant false or absent, and skipped at run time when a computedsh:closedresolves to false, so a broken expression there fails the run only for closed focus nodes. Both are evaluated for literal focus nodes too, so a broken expression fails the run, though a literal has no properties to close.validateClosedShapes = falseskips both.sh:uniqueValuesFor: a computed value is evaluated for every focus node that is not deactivated, whatever the number of targets, so a broken expression fails the run even with one target. Each focus node is compared with the other targets on the properties it resolves to. Each property is indexed once, from its triples. For each distinct list, the targets that share a focus node’s rarest value set are counted once, and every step ticks the run budget.sh:qualifiedMaxCount: a computed one is non-monotone in recursion, like a constant one.sh:reificationRequired: a computed value on a property shape whosesh:pathis not a predicate IRI is reported asREIFIER_CONSTRAINT_ON_COMPLEX_PATH, like the constanttrue. A computed value is evaluated for literal focus nodes too, so a broken expression fails the run, though a literal has no triples to reify.
Unsupported expressions at a parameter. A parameter may hold a constant as well as an expression, so a blank node there is classified more carefully than at sh:values or sh:deactivated:
- A call
[ fn ( ... ) ]of a function the shapes graph does not declare is an unsupported feature (NODE_EXPRESSION). It fails the run by default. WithIGNORE_WITH_WARNINGthe warning ends with “ (the constraints that use it are left out)”: everysh:patternof the shape forsh:flags, both qualified components for a qualified count, the closed constraint forsh:closedandsh:ignoredProperties. - A list at a list parameter (
sh:in,sh:class, …) is a constant. A malformed one is a compile error. - Any other blank node that is neither a constant nor a node expression, such as
sh:minCount [ ex:foo 1 ], is a compile error (ShapeCompileException), whateverunsupportedFeaturessays.
At sh:values, sh:expression and sh:deactivated both kinds are unsupported features, because no constant reading exists there. Detection does not know the kind of shape. An unsupported call at sh:minCount on a node shape, where the compiler ignores sh:minCount, still fails the run. On a property shape, an undeclared call at sh:closed, sh:ignoredProperties or sh:uniqueValuesFor is reported as unsupported although the parameter has no effect there.
SPARQL-based constraint component parameters. A parameter of a sh:ConstraintComponent is computed only when its declaration rules out blank nodes. SHACL 1.2 SPARQL section 4.2.1 makes a value that breaks the declaration ill-formed, so a blank node there cannot be a constant and is read as an expression. The declaration rules out blank nodes when, in its own triples, it has a constant value for one of:
| Declaration | Needed |
|---|---|
sh:nodeKind |
an IRI among sh:IRI, sh:Literal, sh:IRIOrLiteral, sh:TripleTerm, or a non-empty list that holds only these |
sh:datatype |
any IRI or non-empty list (an empty list makes no constraint) |
sh:in |
a list with no blank-node member |
sh:pattern, sh:minLength, sh:maxLength, sh:minInclusive, sh:maxInclusive, sh:minExclusive, sh:maxExclusive |
a literal |
sh:languageIn, sh:rootClass |
any |
sh:class alone does not count, because a SHACL instance can be a blank node. Neither do sh:hasValue, sh:singleLine or a declaration value that is itself an expression. For any other parameter a blank node stays a constant, as before. A declaration that rules out blank nodes also rules out lists, so there a blank node is an expression or a compile error. The output must be one node, not a blank node, and must satisfy the declared sh:nodeKind and sh:datatype. The other declared constraints are not checked, as for constants.
ex:MaxAgeComponent a sh:ConstraintComponent ;
sh:parameter [ sh:path ex:maxAge ; sh:datatype xsd:integer ] ;
sh:validator [ a sh:SPARQLAskValidator ; sh:ask "ASK { FILTER ($value <= $maxAge) }" ] .
ex:P sh:path ex:age ;
ex:maxAge [ shnex:if [ sparql:equals ( [ shnex:pathValues ex:kind ] ex:strict ) ] ; shnex:then 65 ; shnex:else 70 ] .
The computed value is pre-bound like a constant one (see Pre-binding): once for each focus node, shared by every value node of an ASK validator, and {$maxAge} in a message shows it. It comes from the data graph, so it is written into the query text with the same hardened escaping as $this and $value: quotes, line breaks, control characters and backslashes cannot change the structure of the query, whatever \u decoding the provider applies. A value that cannot be written as text, such as a triple term or a directional language string, goes to the provider as a binding. There is no separate path for computed values. A computed triple term needs allowTripleTermsInShapeParameters. A language-tagged pre-bound value written into a plain slot is followed by a space, so that the template text after the variable cannot extend the tag ($p-1 gives "a"@en -1, not the tag en-1); this holds for $this and $value too. Computed values add no cache: queries are built for each focus node anyway.
Dynamic SHACL is evaluated by the native engine only.
SPARQL functions
A sh:ListParameterExpressionFunction whose sh:bodyExpression is a sh:select, a sh:sparqlExpr or a shnex: node expression can be called, by its IRI, from any SHACL-SPARQL query (spec section 7). The engine compiles each declaration and sends the compiled functions with every SHACL-SPARQL query. Jena and RDF4J make them visible to that one query only; no global function registry is ever modified, and a declaration cannot redefine a built-in function (the built-in wins).
- Parameters:
shnex:arg0,shnex:arg1, … form a contiguous run starting at 0, and an optional parameter (sh:optional true) cannot be followed by a mandatory one. Otherwise the shapes graph is ill-formed (ShapeCompileException). - Body: in a SPARQL body,
$thisis the function IRI and$arg<i>holds each argument. Ashnex:body is evaluated as a node expression with the arguments as its parameters. An argument that is absent or failed to evaluate stays unbound if its parameter is optional; otherwise the call is an error. - Errors: a blank-node argument, too many arguments, and a body with no result or several results are errors, as is a call to an unknown function IRI. A function error is an ordinary SPARQL error:
BINDleaves its variable unbound andFILTERis false. It does not fail validation, with one exception: ashnex:body that fails on a conformance check the engine cannot decide (an undefined answer, an unsupported or ill-formed shape, a nested validation error) fails the calling query, so that aFILTERcannot turn the undecided check into a definite answer. This holds also when the body itself wraps the check insparql:coalesceor a similar function, and a node expression that runs the query fails too (see undecided checks fail closed). A timeout inside a body stops the run when the query returns. - Which functions a query calls: a declaration is checked (an ill-formed one is a
ShapeCompileException, a body that is no node expression an unsupportedSHACL_FUNCTION) when some SHACL-SPARQL query may call it. Kastor finds the calls lexically and over-approximates: every IRI reference counts, and so does every prefixed name followed by(. Each query is also read with its\u/\Uescapes decoded, as RDF4J parses it, so an escape cannot hide a call in a string, a comment, an IRI or a name. A relative IRI is resolved against the query’sBASE. A called relative IRI without an absoluteBASEcounts as a call of every declared function, because the provider’s default base is not known. - Recursion: a shape that runs SPARQL (
sh:sparql, a SPARQL-based constraint component, a SPARQL node expression) depends on the shapes that theshnex:bodies of the functions its queries may call validate against (see “which functions a query calls” above, followed through the calls of those bodies), so recursion through a body is decided by the recursion solver; a cycle that makes a shape conform iff it does not is undefined and fails validation. - Nesting: functions can call each other, up to 16 levels deep (
SparqlFunction.MAX_FUNCTION_DEPTH); a deeper call is an error, so recursion terminates. Nested queries are sandboxed like the outer one (noSERVICE, only standard and custom functions) and run on the calling query’s dataset; their ownFROMcan only select graphs inside it. A timeout stops the whole query. Time is the only bound on how many nested calls a query can make (fan-out); SHACL queries always run under the validation timeout.
Limits:
- On both providers a sandboxed query checks its deadline before every expression and while a regular expression matches, and its expressions are bounded in size and cost (
SandboxLimits: a computed string of at most 16 Mi characters, integers of at most 65,536 bits, regular expressions on a linear-time engine, a cap onGROUP_CONCAT, at most 64 Mi computed characters per query over all its rows, metered property paths on Jena). An expression that would exceed a bound fails the query instead of being a SPARQL error, so it cannot make aFILTERhide a violation. - On RDF4J, only stores created by Kastor’s factories evaluate sandboxed queries: a repository that wraps any other store (a plain Sail, an HTTP or SPARQL endpoint) refuses them with
UnsupportedOperationException, as the SPARQL endpoint adapter does. - Parameter constraints (
sh:datatype, …) andsh:defaultValueare not enforced, as the spec says. - A function that is not a
sh:ListParameterExpressionFunction, or whose body is no node expression at all, cannot be called from SPARQL: seeSHACL_FUNCTIONbelow.
Where queries run
When you validate a SPARQL-capable dataset, such as a repository passed to validateDataset, queries run on it in place only if the dataset lists no named graphs. Otherwise the engine copies the data graph into a private in-memory repository once per validation run, and all SPARQL constraints of that run share the copy. This also happens when validating a plain graph. The reason is that a query without a dataset clause may see more than the default graph: on RDF4J it sees the union of all contexts. SHACL-SPARQL constraints must see exactly the data graph that the other constraints validate. Declaring any SPARQL-bodied function, even an unused one, always uses the private copy, on every provider. Each kind of fallback is logged once per dataset class. The copy is not reused across runs; to validate a large graph repeatedly, validate a repository without named graphs in place.
Pre-binding
The engine pre-binds variables itself before the query reaches the provider, so the result is the same on every provider, including inside sub-queries:
- IRIs and literals are substituted textually into the query text. In the outermost
SELECTprojection a value becomes(term AS ?var), soSELECT $thisstill outputs the focus node, as SHACL 1.2 SPARQL Appendix A requires; in a sub-select’s projection it becomes(term AS ?fresh).BOUND(?var)on a substituted variable becomestrue. A language tag is written into the text only up to 256 characters; a longer one is passed as an initial binding. - Values with no equivalent SPARQL syntax are passed to the provider as initial bindings: blank nodes, triple terms, directional language strings, and IRIs containing characters that are illegal in an
IRIREF.
A query that breaks the SHACL-SPARQL pre-binding restrictions is rejected when the shapes are compiled. The restrictions are:
- no
MINUSorSERVICE; - no
VALUESthat mentionsthis, a parameter, or (in ASK queries)value; - no
ASthat re-bindsthis, a parameter, or (in ASK queries)value.
The restrictions are checked on the query text and again on the full query with its PREFIX prologue. Each check is linear in the query length and ticks the validation budget, so ValidationConfig.timeout also bounds the compilation of a hostile query.
The provider enforces the same boundary at run time. Every SHACL-SPARQL query reaches it as a SandboxedSelectQuery, which the Jena and RDF4J providers parse with their own parser and refuse before evaluation if it contains SERVICE anywhere or a custom aggregate, or (on Jena) ARQ’s CALL or fn:apply, a java: IRI or any IRI in ARQ’s namespaces (afn: / apf: functions and property functions, which would load a class by name, and script functions), or an ARQ property-path extension (p{n,m}, …). At run time the functions such a query can call are an allowlist: the SPARQL built-ins and XSD casts the engine implements itself (on Jena also the registered XPath fn: / math: functions) and the query’s own sh:SPARQLFunctions. An engine extension (such as Jena’s afn:wait, which sleeps through the query timeout) or a function an application registered globally is an unknown function there; Jena property functions are off, and SERVICE has no executor. Validation then throws ShaclValidationException. A remote SPARQL endpoint cannot be checked, so a dataset backed by one is validated on a private in-memory copy of the data graph instead.
Sub-selects need not project $this (SHACL 1.2 removed that restriction). One limit remains: a blank-node value used inside a sub-select that does not project it cannot be passed to the provider. The provider contract rejects it, and validation throws ShaclValidationException.
$PATH (either $ or ?) is substituted on property shapes and left unbound on node shapes.
Other VALUES clauses are allowed, as SHACL 1.2 Appendix A specifies. The approved W3C test unsupported-sparql-002 still expects any VALUES to fail (the SHACL 1.0 rule); the harness lists it as a spec override (see below).
Validation then throws ShaclValidationException (“SHACL compile failed: …”), with a SparqlPreBindingRestrictionException as its cause.
Unsupported features
The native engine recognises some SHACL constructs that it cannot evaluate. By default (ValidationConfig.unsupportedFeatures = UnsupportedFeatureHandling.FAIL), it rejects the shapes graph before validating anything, so these constraints are never silently skipped.
UnsupportedShaclFeature |
Constructs |
|---|---|
SPARQL_CONSTRAINT_COMPONENT |
None: SPARQL-based constraint components are supported (see above). The category is kept for compatibility |
NODE_EXPRESSION |
SHACL 1.2 node expressions the engine cannot evaluate: a blank node at a node expression position (sh:values, sh:defaultValue, sh:targetNode, sh:deactivated of a shape, an sh:sparql constraint or a reifier, sh:expression, sh:nodeByExpression) that is no node expression of the library, such as a call of a function the shapes graph does not declare. At a computed constraint parameter only a call of an undeclared function counts (see Dynamic SHACL) |
SPARQL_NODE_EXPRESSION |
SPARQL node expressions (sh:select, sh:sparqlExpr) used as sh:targetWhere values. At every node expression position (sh:targetNode, sh:values, sh:defaultValue, sh:expression, sh:nodeByExpression) they are supported |
SHACL_FUNCTION |
SHACL 1.2 functions that a SPARQL query calls but the engine cannot compile: not a sh:ListParameterExpressionFunction, or a sh:bodyExpression that is no node expression at all. Functions with a sh:select / sh:sparqlExpr or shnex: body are supported (see above) |
CUSTOM_TARGET |
SPARQL-based or custom targets (sh:target) |
REIFIER_CONSTRAINT_ON_COMPLEX_PATH |
sh:reifierShape / sh:reificationRequired true on a property shape whose sh:path is not a predicate IRI (the engine only knows which triples a predicate path traverses) |
sh:sparql constraints, SPARQL-based constraint components, node expressions and SPARQL functions (see above) are supported. Still unsupported are SPARQL node expressions at sh:targetWhere (SPARQL_NODE_EXPRESSION) and blank nodes at node expression positions that are no node expression (NODE_EXPRESSION). A non-constant sh:deactivated on a reifier is supported (see above).
Dynamic SHACL (computed constraint parameters) is supported (see above). These parameters stay constant, because their value may be a blank node: the shape references (sh:node, sh:property, sh:qualifiedValueShape, sh:not, sh:and, sh:or, sh:xone, sh:memberShape, sh:someValue, sh:shape, sh:reifierShape), sh:hasValue, the property path parameters (sh:path, sh:equals, sh:disjoint, sh:lessThan, sh:lessThanOrEquals, sh:subsetOf), sh:sparql, and the parameters of a SPARQL-based constraint component whose declaration does not rule out blank nodes. At a parameter, a call of an undeclared function is NODE_EXPRESSION; any other blank node that is no constant and no node expression is a compile error.
In the failure, ShaclValidationException wraps an UnsupportedShaclFeatureException. Its message names each offending construct, and its features property lists the categories:
try {
validator.validate(dataGraph, shapesGraph)
} catch (e: ShaclValidationException) {
val unsupported = e.cause as? UnsupportedShaclFeatureException ?: throw e
println("Unsupported: ${unsupported.features}")
}
To skip these constructs instead, set ValidationConfig(unsupportedFeatures = UnsupportedFeatureHandling.IGNORE_WITH_WARNING). Each skipped construct is listed in report.warnings (“Unsupported SHACL feature ignored: …”). These are report warnings, not validation results, so they do not change isValid. toShaclValidationReportRdf() exports them as ksh:warning literals on the report node, so the RDF report does not say sh:conforms true without a trace of what was skipped. ValidationConfig(includeWarnings = false) leaves them out of the report (and of its RDF export).
Detection only inspects declared and implicit shapes, plus the nodes reachable from them through shape-valued parameters. Data triples that share the shapes graph are therefore never mistaken for expressions. For example, validate(g, g) works when g has blank-node sh:targetNode values that carry data triples.
Blank node sh:targetNode values. In SHACL 1.2 the values of sh:targetNode are node expressions: an IRI or a literal is a constant, a blank node is a computed expression. A blank node of the shapes graph can only denote a data node when the two graphs share it (the same graph validated against itself, or shapes discovered from the data). The engine therefore applies this rule:
- A blank node whose own triples use vocabulary that has no reading as data is a node expression, evaluated whatever the data graph is: a SPARQL expression (
sh:select,sh:sparqlExpr), theshnex:orsparql:namespaces as predicate or type (shnex:pathValues,sparql:concat ( … )), or a call[ ex:fn ( … ) ]of a function declared in the shapes graph. - Any other blank node is an ordinary target if it is a node of the data graph, regardless of its predicates: a plain node, an RDF list cell, a node that carries
sh:predicates or a SHACL type (a shape used as data), or a call-shaped node whose function is not declared. - Otherwise it can never be a focus node, and the shape would silently go unvalidated. It is reported as an unsupported
NODE_EXPRESSION: a failure by default, a report warning withIGNORE_WITH_WARNING. This is the same for every such blank node, whatever its triples look like.
Configuration options that are rejected or have no effect
parallelValidation = trueandstreamingMode = trueare not supported by the native engine (provider idskastorandmemory). Creating a validator with either throwsUnsupportedShaclOperationException, aShaclValidationException.validate(graph, List<ShaclShape>)andvalidateConstraints(graph, constraints)throw the same exception for a non-empty list: pass the shapes as an RDF graph. The RDF4J bridge throwsUnsupportedOperationExceptionfor these two calls.includeWarnings = falseemptiesreport.warningsand nothing else: results are reported whatever their severity, includingsh:Warning.batchSize,enableExplanations,enableSuggestionsandvalidateInactiveShapesare deprecated. No engine ever read them: validation is not batched,explanationandsuggestedFixare never filled, and a shape withsh:deactivated trueis never validated. They remain only for binary compatibility.
W3C conformance
CI runs the full W3C SHACL 1.2 test suite (core, node-expression and SPARQL manifests) against the native engine. The suite comes from a pinned w3c/data-shapes commit (94d8bc2: 166 sht:Validate cases plus 144 sht:EvalNodeExpr cases, 310 in total). To run it locally:
python scripts/fetch-conformance-data.py --only shacl
./gradlew :rdf:shacl-validation:w3cConformanceTest
The task fails instead of skipping when the suite is missing, and every case is executed. The default test task runs the same harness against the bundled fixture when the checkout is missing; a missing manifest, a manifest without sht:Validate entries or one without an approved case fails the test rather than skipping it, unless -PshaclW3cAllowMissingData=true opts out. Manifest rows that are not approved are skipped unless -PshaclW3cIncludeNonApproved=true. The harness checks failures strictly:
- All 144
sht:EvalNodeExprcases and allsht:Validatecases pass on the default provider (Jena), excepthasLang.ttl, which is accepted as ill-formed (spec override, see below).W3cKnownDeviationsholds 0 unsupported-feature deviations; a deviation would need anUnsupportedShaclFeaturecategory and must fail with anUnsupportedShaclFeatureExceptionfor exactly that category. - Spec overrides are approved cases whose expected result contradicts the normative SHACL 1.2 text. The engine follows the text. There is 1 in
W3cKnownDeviations:unsupported-sparql-002expects anyVALUESto fail, but SHACL 1.2 Appendix A only forbidsVALUESthat mentions a pre-bound variable. The node expression harness lists 1 ill-formed case:node-expr/shnex-sparql/hasLang.ttlcallssparql:hasLangwith 2 arguments, but the spec table and SPARQL 1.2 define 1, so Kastor treats the call as ill-formed (ShapeCompileException). - On RDF4J 5.3, some SPARQL 1.2 functions are missing (
hasLang,hasLangdir,langdir,strlangdir,sameValue,isTriple,triple,subject,predicate,object), and asparql:call of one fails there. RDF4J also returns different lexical forms for some numeric results (ceil,floor,round,divide,seconds). sht:Failurecases must fail with their expected category (a pre-binding restriction or a shapes compile failure), never with an unsupported feature.sh:conformanceDisallowscases check the engine’s ownisValidand exportedsh:conforms.- Result graphs are compared after removing the
ksh:triples, so an undecided result would look like a failure. A case therefore fails if the engine emits any result with aksh:resultStatus(undefined recursion, pattern timeout, pattern too complex), unless the case is listed with a reason inW3cKnownDeviations. No case is listed: the engine decides every constraint of the suite.
See the module README.
RDF4J validator
With providerId = "rdf4j", isValid comes from the engine’s sh:conforms, read before any maxViolations cap is applied. validateResource validates the whole graph and then keeps only the results for the requested focus node.
Tip: Use the SHACL DSL to create shapes graphs more easily. The DSL supports all SHACL 1.2 features including:
- SHACL 1.2 Core:
targetWherewith node expressions,shapetargets,singleLineconstraint,reifierShapeandreificationRequiredfor RDF-star- SHACL 1.2 SPARQL Extensions: SPARQL-based constraints using SELECT queries (
sparqlAskwas removed:sh:sparqlconstraints cannot use ASK; ASK is available only as a constraint componentsh:validator)See How to Create SHACL Shapes for examples. For bundled ontology-quality shapes, the
onto-qaCLI, and the optional embedding tier, see How to Check Ontology Quality.
Notes
ValidationContextis only enforced when provided.ValidationResult.NotConfiguredis returned when no context is available.- Repository-level validation remains independent of Kastor Gen materialization.