Skip to content

Issue 189: Add sh:nodeByExpression Constraint Component #408

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: gh-pages
Choose a base branch
from
237 changes: 236 additions & 1 deletion shacl12-core/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2531,6 +2531,7 @@ <h2>Node Expressions</h2>
<ul>
<li>At <a href="#property-shapes"><code>sh:values</code> and <code>sh:defaultValue</code></a> to derive the value nodes of a property shape.</li>
<li>At <a href="#targetNode"><code>sh:targetNode</code></a> to dynamically compute the targets of a shape.</li>
<li>At <a href="#NodeByExpressionConstraintComponent"><code>sh:nodeByExpression</code></a> to validate nodes against a dynamically computed set of node shapes.</li>
<li>At <a href="#ExpressionConstraintComponent"><code>sh:expression</code></a> to validate nodes against a condition.</li>
<li>At <a href="#deactivated"><code>sh:deactivated</code></a> to deactivate certain shapes under specific conditions.</li>
</ul>
Expand Down Expand Up @@ -2821,7 +2822,7 @@ <h3>Conformance Checking</h3>
has been reported by it.
</p>
<p>
<dfn>Conformance checking</dfn> produces <code>true</code> if and only if a given <a>focus node</a>
<dfn data-lt="conformance check">Conformance checking</dfn> produces <code>true</code> if and only if a given <a>focus node</a>
<a>conforms</a> to a given <a>shape</a>, and <code>false</code> otherwise.
</p>
<p id="conformance-nested">
Expand Down Expand Up @@ -6619,6 +6620,239 @@ <h4>sh:reifierShape, sh:reificationRequired</h4>
}
}
}
}</pre>
</div>
</div>
</aside>
</section>

<section id="NodeByExpressionConstraintComponent">
<h4>sh:nodeByExpression</h4>
<p>
<code>sh:nodeByExpression</code> specifies the condition that each <a>value node</a> conforms to the
<a>node shapes</a> produced by a <a>node expression</a>.
The evaluation of these node expressions is repeated for all <a>value nodes</a> of the <a>shape</a>
as the <a>focus node</a>.
</p>
<p>
<span class="component-class">Constraint Component IRI</span>: <code>sh:NodeByExpressionConstraintComponent</code>
</p>

<div class="parameters">Parameters:</div>
<table class="term-table">
<tr>
<th>Property</th>
<th>Summary and Syntax Rules</th>
</tr>
<tr>
<td><code>sh:nodeByExpression</code></td>
<td>
The <a>node shapes</a> that all value nodes need to conform to.
<span data-syntax-rule="nodeByExpression-scope">The <a>values</a> of <code>sh:nodeByExpression</code> in a shape must be <a>well-formed</a> <a>node expressions</a>.</span>
</td>
</tr>
</table>
<div class="def def-text">
<div class="def-header">TEXTUAL DEFINITION</div>
<div class="def-text-body" data-validator="NodeByExpression">
Let <code>$expr</code> be a <a>value</a> of <code>sh:nodeByExpression</code>.
For each <a>value node</a> <code>v</code>: perform a <a>conformance check</a> of
<code>v</code> against each <a>output node</a> of <code>evalExpr(expr,
<a>data graph</a>, v, {})</code> <code>s</code>. A <a>failure</a>
MUST be produced if the <a>conformance check</a> of <code>v</code> against
<code>s</code> produces a <a>failure</a>. Otherwise, if <code>v</code> does
not <a>conform</a> to <code>s</code>, there is a <a>validation result</a>
with <code>v</code> as <code>sh:value</code> and a <a>deep copy</a> of
<code>s</code> as <code>sh:sourceConstraint</code>.
</div>
</div>
<p><em>The remainder of this section is informative.</em></p>
<p>
<code>sh:nodeByExpression</code> functions similarly to <code>sh:node</code>, but instead of referencing a fixed <a>node shape</a>,
a referenced <a>node expression</a> is used to dynamically compute the set of <a>node shapes</a> to which each <a>value node</a> must conform.
</p>
<p>
There are three key differences between <code>sh:nodeByExpression</code> and <a href="#NodeConstraintComponent"><code>sh:node</code></a>:
<ol>
<li>
<code>sh:nodeByExpression</code> references a <a>node expression</a> instead of a fixed <a>node shape</a> as <code>sh:node</code> does.
</li>
<li>
<code>sh:nodeByExpression</code> cannot reference a <a>node shape</a> that is a <a>blank node</a> as a value like <code>sh:node</code> can,
as a <a>blank node</a> would be interpreted as a <a>node expression</a>.
</li>
<li>
<a>Results</a> generated by <code>sh:nodeByExpression</code> additionally include a value for `sh:sourceConstraint`.
</li>

</ol>
</p>
<p>
Note that <code>sh:node</code> and <code>sh:nodeByExpression</code> exhibit the same behavior when given a <a>value</a> that is an <a>IRI</a> of a <a>node shape</a>.
In this case, <code>sh:node</code> directly validates against the specified <a>node shape</a>, whereas <code>sh:nodeByExpression</code> interprets the <a>IRI</a>
as an <a>IRI expression</a> that evaluates to a set containing the same <a>node shape</a>.
</p>
<p>
In the following example, all values of the property <code>ex:address</code> must fulfill the
constraints expressed by the <a>shape</a> <code>ex:AddressShape</code>.
</p>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An example at this point showing where it is different to sh:node would be useful.

Copy link
Author

@mgberg mgberg Jun 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An example was removed as per the discussion above (here and here).

There are two differences between sh:nodeByExpression and sh:node within the scope of the core spec:

  1. sh:nodeByExpression cannot take a node shape that is a blank node as sh:node can, as a blank node would be interpreted as a Node Expression.
  2. Results generated by sh:nodeByExpression additionally include a value for sh:sourceConstraint.

The example I originally included was essentially a duplicate of the example for sh:node with the constraint component replaced and a value for sh:sourceConstraint in the example result. @HolgerKnublauch suggested I remove it and wait to put a more comprehensive example in the Node Expression spec.

I can certainly revert that change if it is desired to have that example back.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation.

As a general comment, I'm not sure we are helping users by not including examples that show clear node expressions in Core but that's not for this PR.

(Do people really think there will be common static only SHACL 1.2 implementations?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Do people really think there will be common static only SHACL 1.2 implementations?)

@afs I mean there are some SHACL 1.0 ones that don't support SPARQL-based constraints such as https://github.com/zazuko/rdf-validate-shacl#limitations (on which https://shacl-playground.zazuko.com/ is based) for example.

There are two differences between sh:nodeByExpression and sh:node within the scope of the core spec:

1. `sh:nodeByExpression` cannot take a node shape that is a blank node as `sh:node` can, as a blank node would be interpreted as a Node Expression.

2. Results generated by `sh:nodeByExpression` additionally include a value for `sh:sourceConstraint`.

@mgberg I like this description as well.. can't we add this as well to the text?

The example I originally included was essentially a duplicate of the example for sh:node with the constraint component replaced and a value for sh:sourceConstraint in the example result. @HolgerKnublauch suggested I remove it and wait to put a more comprehensive example in the Node Expression spec.

I can certainly revert that change if it is desired to have that example back.

I don't think one can have too much examples, esp. if they help clearing things up.
+1 to adding it back

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@simonstey I modified the description in the spec to include those points explicitly, and I also added the example back.

<aside class="example">
<div class="shapes-graph">
<div class="turtle">
ex:AddressShape
a sh:NodeShape ;
sh:property [
sh:path ex:postalCode ;
sh:datatype xsd:string ;
sh:maxCount 1 ;
] .

ex:PersonShape
a sh:NodeShape ;
<span class="target-can-be-skipped">sh:targetClass ex:Person ;</span>
sh:property [ # _:b1
sh:path ex:address ;
sh:minCount 1 ;
sh:nodeByExpression ex:AddressShape ;
] .
</div>
<div class="jsonld">
<pre class="jsonld">{
"@graph": [
{
"@id": "ex:AddressShape",
"@type": "sh:NodeShape",
"sh:property": {
"sh:datatype": {
"@id": "xsd:string"
},
"sh:maxCount": {
"@type": "xsd:integer",
"@value": "1"
},
"sh:path": {
"@id": "ex:postalCode"
}
}
},
{
"@id": "ex:PersonShape",
"@type": "sh:NodeShape",
"sh:property": {
"sh:minCount": {
"@type": "xsd:integer",
"@value": "1"
},
"sh:nodeByExpression": {
"@id": "ex:AddressShape"
},
"sh:path": {
"@id": "ex:address"
}
},
"sh:targetClass": {
"@id": "ex:Person"
}
}
]
}</pre>
</div>
</div>
<div class="data-graph">
<div class="turtle">
ex:Bob a ex:Person ;
ex:address ex:BobsAddress .

ex:BobsAddress
ex:postalCode "1234" .

<span class="focus-node-error">ex:Reto</span> a ex:Person ;
ex:address ex:RetosAddress .

ex:RetosAddress
ex:postalCode 5678 .
</div>
<div class="jsonld">
<pre class="jsonld">{
"@graph": [
{
"@id": "ex:Bob",
"@type": "ex:Person",
"ex:address": {
"@id": "ex:BobsAddress"
}
},
{
"@id": "ex:BobsAddress",
"ex:postalCode": "1234"
},
{
"@id": "ex:Reto",
"@type": "ex:Person",
"ex:address": {
"@id": "ex:RetosAddress"
}
},
{
"@id": "ex:RetosAddress",
"ex:postalCode": {
"@type": "xsd:integer",
"@value": "5678"
}
}
]
}</pre>
</div>
</div>
<div class="results-graph">
<div class="turtle">
[ a sh:ValidationReport ;
sh:conforms false ;
sh:result [
a sh:ValidationResult ;
sh:resultSeverity sh:Violation ;
sh:focusNode ex:Reto ;
sh:resultPath ex:address ;
sh:value ex:RetosAddress ;
sh:resultMessage "Value does not conform to shape ex:AddressShape." ;
sh:sourceConstraint ex:AddressShape ;
sh:sourceConstraintComponent sh:NodeByExpressionConstraintComponent ;
sh:sourceShape _:b1 ;
]
] .
</div>
<div class="jsonld">
<pre class="jsonld">{
"@type": "sh:ValidationReport",
"sh:conforms": {
"@type": "xsd:boolean",
"@value": "false"
},
"sh:result": {
"@type": "sh:ValidationResult",
"sh:focusNode": {
"@id": "ex:Reto"
},
"sh:resultMessage": "Value does not conform to shape ex:AddressShape.",
"sh:resultPath": {
"@id": "ex:address"
},
"sh:resultSeverity": {
"@id": "sh:Violation"
},
"sh:sourceConstraint": {
"@id": "ex:AddressShape"
},
"sh:sourceConstraintComponent": {
"@id": "sh:NodeByExpressionConstraintComponent"
},
"sh:sourceShape": {
"@id": "_:b66_b1"
},
"sh:value": {
"@id": "ex:RetosAddress"
}
}
}</pre>
</div>
</div>
Expand Down Expand Up @@ -7525,6 +7759,7 @@ <h2>Changes between SHACL 1.0 Core and SHACL 1.2 Core</h2>
<li>Added the new class <a href="#ShapeClass"><code>sh:ShapeClass</code></a> for implicit class targets; see <a href="https://github.com/w3c/data-shapes/issues/212">Issue 212</a></li>
<li>Moved SPARQL-based validators from Core to an Appendix of SHACL-SPARQL; see <a href="https://github.com/w3c/data-shapes/issues/271">Issue 271</a></li>
<li>Added the new constraint component <a href="#ExpressionConstraintComponent"><code>sh:expression</code></a>; see <a href="https://github.com/w3c/data-shapes/issues/357">Issue 357</a></li>
<li>Added the new constraint component <a href="#NodeByExpressionConstraintComponent"><code>sh:nodeByExpression</code></a>, see <a href="https://github.com/w3c/data-shapes/issues/408">Issue 408</a></li>
<li>Added the new value <code>sh:ByTypes</code> for <a href="#ClosedConstraintComponent"><code>sh:closed</code></a>; see <a href="https://github.com/w3c/data-shapes/issues/172">Issue 172</a></li>
<li>The values of <a href="#ClassConstraintComponent"><code>sh:class</code></a> and <a href="#DatatypeConstraintComponent"><code>sh:datatype</code></a> can now also be lists, indicating a union of choices; see <a href="https://github.com/w3c/data-shapes/issues/160">Issue 160</a></li>
</ul>
Expand Down
1 change: 1 addition & 0 deletions shacl12-test-suite/tests/core/node/manifest.ttl
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
mf:include <minLength-001.ttl> ;
mf:include <minListLength-001.ttl> ;
mf:include <node-001.ttl> ;
mf:include <nodeByExpression-001.ttl> ;
mf:include <nodeKind-001.ttl> ;
mf:include <not-001.ttl> ;
mf:include <not-002.ttl> ;
Expand Down
59 changes: 59 additions & 0 deletions shacl12-test-suite/tests/core/node/nodeByExpression-001.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
@prefix dash: <http://datashapes.org/dash#> .
@prefix ex: <http://datashapes.org/sh/tests/core/node/nodeByExpression-001.test#> .
@prefix mf: <http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix sht: <http://www.w3.org/ns/shacl-test#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ex:InvalidInstance
rdf:type ex:TestClass ;
rdfs:label "Invalid instance" ;
.
ex:TestClass
rdf:type rdfs:Class ;
rdf:type sh:NodeShape ;
rdfs:label "Test class" ;
rdfs:subClassOf rdfs:Resource ;
# Only using an IRI Expression here because Core doesn't define interesting node expressions
sh:nodeByExpression ex:TestNodeShape ;
.
ex:TestNodeShape
rdf:type sh:NodeShape ;
sh:class ex:OtherClass ;
.
ex:ValidInstance
rdf:type ex:OtherClass ;
rdf:type ex:TestClass ;
rdfs:label "Valid instance" ;
.
<>
rdf:type mf:Manifest ;
mf:entries (
<nodeByExpression-001>
) ;
.
<nodeByExpression-001>
rdf:type sht:Validate ;
rdfs:label "Test of sh:nodeByExpression at node shape 001" ;
mf:action [
sht:dataGraph <> ;
sht:shapesGraph <> ;
] ;
mf:result [
rdf:type sh:ValidationReport ;
sh:conforms "false"^^xsd:boolean ;
sh:result [
rdf:type sh:ValidationResult ;
sh:focusNode ex:InvalidInstance ;
sh:resultSeverity sh:Violation ;
sh:sourceConstraint ex:TestNodeShape ;
sh:sourceConstraintComponent sh:NodeConstraintComponent ;
sh:sourceShape ex:TestClass ;
sh:value ex:InvalidInstance ;
] ;
] ;
mf:status sht:approved ;
.
1 change: 1 addition & 0 deletions shacl12-test-suite/tests/core/property/manifest.ttl
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
mf:include <minListLength-001.ttl> ;
mf:include <node-001.ttl> ;
mf:include <node-002.ttl> ;
mf:include <nodeByExpression-001.ttl> ;
mf:include <nodeKind-001.ttl> ;
mf:include <not-001.ttl> ;
mf:include <or-001.ttl> ;
Expand Down
Loading