The previous post used SHACL as one
tool among several to check SKOS's own integrity conditions. This post
looks at SHACL on its own terms: a shape is a reusable contract —
"a foaf:Person must have at least one name" — checked against any
graph, independent of whatever vocabulary that graph happens to use.
Factoidal's SHACL Core validator scores 120 pass, 0 fail (of 120: 98
core constraint-component tests plus 22 sh:sparql-based tests,
including custom constraint components) against the W3C test suite —
see the test-results dashboard for the
current run.
foaf:Person#Back to post 01's Alice
and Bob. A shape requiring every foaf:Person to have a name, and
every foaf:knows edge to point at another foaf:Person:
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <http://example.org/shapes/> .
ex:PersonShape
a sh:NodeShape ;
sh:targetClass foaf:Person ;
sh:property [
sh:path foaf:name ;
sh:minCount 1 ;
sh:message "foaf:Person needs at least one foaf:name" ;
] ;
sh:property [
sh:path foaf:name ;
sh:datatype xsd:string ;
sh:message "foaf:name must be a plain xsd:string" ;
] ;
sh:property [
sh:path foaf:knows ;
sh:class foaf:Person ;
sh:message "foaf:knows must point to another foaf:Person" ;
] .
Three property shapes, three distinct constraint kinds:
sh:minCount (cardinality — at least one value), sh:datatype (the
literal's datatype must match), sh:class (the value must belong to a
class). All three target the same path or a sibling one; each fails
independently of the others.
Alice and Bob, unchanged from post 01, against the shape above:
PERSON_SHAPE_TTL = `
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <http://example.org/shapes/> .
ex:PersonShape
a sh:NodeShape ;
sh:targetClass foaf:Person ;
sh:property [
sh:path foaf:name ;
sh:minCount 1 ;
sh:message "foaf:Person needs at least one foaf:name" ;
] ;
sh:property [
sh:path foaf:name ;
sh:datatype xsd:string ;
sh:message "foaf:name must be a plain xsd:string" ;
] ;
sh:property [
sh:path foaf:knows ;
sh:class foaf:Person ;
sh:message "foaf:knows must point to another foaf:Person" ;
] .
`
The shape is named once, above, and every cell below references
PERSON_SHAPE_TTL by name rather than repeating it:
const PEOPLE_TTL = `
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix ex: <http://example.org/> .
ex:alice a foaf:Person ;
foaf:name "Alice" ;
foaf:knows ex:bob .
ex:bob a foaf:Person ;
foaf:name "Bob" .
`;
const result = await fn.shaclValidate(PEOPLE_TTL, PERSON_SHAPE_TTL);
return { conforms: result.conforms, reportSize: result.report.size };
conforms: true — both Alice and Bob have a foaf:name, it's a plain
string in both cases, and Bob (the only foaf:knows target here) is
himself a foaf:Person. result.report is still a real Dataset
even when there's nothing wrong — sh:conforms true plus a
sh:ValidationReport type triple, no sh:ValidationResult entries.
Delete Bob's foaf:name and validate again:
const NO_NAME_TTL = `
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix ex: <http://example.org/> .
ex:alice a foaf:Person ;
foaf:name "Alice" ;
foaf:knows ex:bob .
ex:bob a foaf:Person .
`;
const RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
const SH_VALIDATION_RESULT = "http://www.w3.org/ns/shacl#ValidationResult";
const result = await fn.shaclValidate(NO_NAME_TTL, PERSON_SHAPE_TTL);
const quads = [...result.report];
const rows = quads
.filter((q) => q.predicate.value === RDF_TYPE && q.object.value === SH_VALIDATION_RESULT)
.map((resultTypeQuad) => {
const resultNode = resultTypeQuad.subject.value;
const fieldOf = (predSuffix) => {
const hit = quads.find(
(q) => q.subject.value === resultNode && q.predicate.value.endsWith(predSuffix)
);
return hit ? hit.object.value : null;
};
return {
focusNode: fieldOf("#focusNode"),
path: fieldOf("#resultPath"),
message: fieldOf("#resultMessage"),
};
});
return { conforms: result.conforms, violations: rows };
One violation row: focus node ex:bob, path foaf:name, message
"foaf:Person needs at least one foaf:name" — the report is itself an
RDF graph (sh:ValidationReport/sh:ValidationResult, SHACL §3), read
here the same way every other post's live cells read a Dataset:
iterate the quads, filter by predicate.
sh:minCount, sh:datatype, and sh:class each catch a different
kind of mistake. Break the graph three separate ways, one constraint
at a time, and validate each against the same shape:
const MIN_COUNT_VIOLATION_TTL = `
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix ex: <http://example.org/> .
ex:alice a foaf:Person ; foaf:name "Alice" ; foaf:knows ex:bob .
ex:bob a foaf:Person .
`;
const DATATYPE_VIOLATION_TTL = `
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <http://example.org/> .
ex:alice a foaf:Person ; foaf:name "42"^^xsd:integer ; foaf:knows ex:bob .
ex:bob a foaf:Person ; foaf:name "Bob" .
`;
const CLASS_VIOLATION_TTL = `
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix ex: <http://example.org/> .
ex:alice a foaf:Person ; foaf:name "Alice" ; foaf:knows ex:mallory .
ex:mallory foaf:name "Mallory" .
`;
const SH_COMPONENT = "http://www.w3.org/ns/shacl#sourceConstraintComponent";
async function violatedComponent(dataTtl) {
const result = await fn.shaclValidate(dataTtl, PERSON_SHAPE_TTL);
const quads = [...result.report];
const componentQuad = quads.find((q) => q.predicate.value === SH_COMPONENT);
return {
conforms: result.conforms,
component: componentQuad ? componentQuad.object.value.split("#")[1] : null,
};
}
return {
minCount: await violatedComponent(MIN_COUNT_VIOLATION_TTL),
datatype: await violatedComponent(DATATYPE_VIOLATION_TTL),
class: await violatedComponent(CLASS_VIOLATION_TTL),
};
Each broken graph is otherwise identical to the conforming one — one
triple changed, one constraint tripped:
MinCountConstraintComponent when Bob's foaf:name is missing
entirely, DatatypeConstraintComponent when Alice's name is an
xsd:integer instead of a plain string, ClassConstraintComponent
when Alice's foaf:knows points at ex:mallory, who is never typed
foaf:Person. sh:sourceConstraintComponent is SHACL's own way of
naming which constraint fired — the same IRI the W3C test suite's 98
core-constraint tests check against.
Other posts in this series cover the other RDF syntaxes, SPARQL Update and the HTTP protocol, ShEx, JSON-LD, RDFC-1.0 canonicalization, and the verification story. See the series plan for the full map.
Every live cell above is pinned in
tests/hub/post05_test.mjs —
the exact same source, executed against the real npm/factoidal
typed API instead of the in-browser fn adapter.