SHACL validation (validateWithShacl)
You only need this section if you want to validate ABox RDF quads against SHACL shapes. This is the inverse of
toShacl(): emit shapes once, validate instances against them repeatedly.
jt.validateWithShacl(shapes, data) runs an in-process SHACL validator over a set of ABox quads. It accepts the OntologyBuilder returned by toShacl() directly, or a raw QuadInterface[] shape array. The data quads typically come from toQuads().
Declaration
jt.validateWithShacl(
shapes: OntologyBuilder | readonly QuadInterface[],
data: readonly QuadInterface[]
): ShaclValidationReportInterfaceshapes: the SHACL shape set. Pass jt.toShacl() directly (the most common form), or a pre-built QuadInterface[] from shaclQuads(). Shapes are built from the schemas registered in the current JsonTology instance.
data: the ABox quad array to validate. Use jt.toQuads(schema, value) to produce it from a typed instance, or hand-craft quads when testing non-conforming scenarios (see example 113).
Returns. A ShaclValidationReportInterface:
| Field | Type | Description |
|---|---|---|
conforms | boolean | true when all constraints pass |
results | ShaclValidationResultInterface[] | One entry per constraint violation |
ShaclValidationResultInterface
Each violation entry in report.results carries:
| Field | Type | Description |
|---|---|---|
focusNode | string | IRI of the node that failed the shape constraint |
resultPath | string | Predicate IRI of the property that failed |
resultSeverity | 'Violation' | 'Warning' | 'Info' | SHACL severity level |
sourceConstraintComponent | string | IRI of the constraint component that fired |
value | unknown | The offending value, when available |
resultMessage | string | Human-readable description of the violation |
Constraint components covered
validateWithShacl evaluates all SHACL shapes produced by toShacl(). The shape set includes:
| Component | SHACL IRI | Triggered by |
|---|---|---|
sh:MinCountConstraintComponent | sh:minCount | Required properties absent |
sh:MaxCountConstraintComponent | sh:maxCount | Properties exceeding cardinality |
sh:DatatypeConstraintComponent | sh:datatype | Literal value with wrong XSD type |
sh:MinLengthConstraintComponent | sh:minLength | String shorter than minLength |
sh:MaxLengthConstraintComponent | sh:maxLength | String longer than maxLength |
sh:PatternConstraintComponent | sh:pattern | String not matching pattern |
sh:MinInclusiveConstraintComponent | sh:minInclusive | Number below minimum |
sh:MaxInclusiveConstraintComponent | sh:maxInclusive | Number above maximum |
sh:MinExclusiveConstraintComponent | sh:minExclusive | Number not above exclusiveMinimum |
sh:MaxExclusiveConstraintComponent | sh:maxExclusive | Number not below exclusiveMaximum |
sh:NodeKindConstraintComponent | sh:nodeKind | Subject is wrong node kind (IRI vs literal) |
sh:ClassConstraintComponent | sh:class | Object does not have the expected rdf:type |
Bounded scope
validateWithShacl validates shapes produced by toShacl() and ShaclProjection. It:
- Uses implicit class targeting: each
sh:NodeShapeis matched to subjects whoserdf:typequad names the corresponding class IRI. Focus nodes without a matchingrdf:typeare not evaluated. - Evaluates only the property constraints encoded in the shapes — structural JSON Schema constraints that
ShaclProjectionmaps to SHACL predicates. - Does not perform OWL reasoning over the TBox. Class disjointness and equivalence axioms from the TBox are not checked during SHACL validation.
Examples
Example 1: Conforming and non-conforming instances
/**
* Advanced Example 113 — validateWithShacl
*
* `validateWithShacl(shapes, data)` is the inverse of `toShacl()`:
*
* toShacl() → SHACL shape quads encoding structural constraints
* validateWithShacl() → conformance report against those shapes + ABox data
*
* The method accepts either an `OntologyBuilder` (the return of `toShacl()`)
* or a raw `QuadInterface[]` shape array. ABox data quads come from
* `toQuads()`.
*
* Conforming instance: `report.conforms === true`, `report.results` is empty.
* Non-conforming instance (required property absent): `report.conforms === false`
* and `report.results` contains a `MinCountConstraintComponent` entry.
*
* Note: `toQuads()` validates the data before projecting, so non-conforming
* data quads must be crafted manually. The predicate IRI used in those quads
* must match the `sh:path` IRI emitted by `toShacl()` — both go through
* JsonTology's predicateResolver, so they are always consistent.
*
* @since 0.20.0
*/
import type { QuadInterface } from '../../../src/interfaces/QuadInterface.js';
import type { ShaclValidationReportType } from '../../../src/types/ShaclValidationReportType.js';
import { JsonTology } from '../../../src/index.js';
import { Terms } from '../../../src/modules/quads/Terms.js';
import {
RDF, SH
} from '../../../src/constants/IRI.js';
// ── Schema definitions ────────────────────────────────────────────────────
// A minimal bookstore domain: Author requires `name`; Book requires `title`.
// Primitives with constraints have their own $id and are referenced via $ref.
const BASE = 'https://bookstore.example.com';
const AuthorNameSchema = {
'$id': `${BASE}/AuthorName`,
'minLength': 1,
'type': 'string'
} as const;
const TitleSchema = {
'$id': `${BASE}/Title`,
'minLength': 1,
'type': 'string'
} as const;
const AuthorSchema = {
'$id': `${BASE}/Author`,
'properties': { 'name': { '$ref': `${BASE}/AuthorName` } },
'required': ['name'],
'type': 'object'
} as const;
const BookSchema = {
'$id': `${BASE}/Book`,
'properties': {
'author': { '$ref': `${BASE}/Author` },
'title': { '$ref': `${BASE}/Title` }
},
'required': ['title'],
'type': 'object'
} as const;
const jt = JsonTology.create({
'baseIri': BASE,
'schemas': [
AuthorNameSchema,
TitleSchema,
AuthorSchema,
BookSchema
] as const
});
// ── Build shapes once ────────────────────────────────────────────────────
// toShacl() returns an OntologyBuilder; validateWithShacl accepts it directly.
const shapes = jt.toShacl();
// ── Case 1: CONFORMING instance ───────────────────────────────────────────
// A valid Book (title present) projected through toQuads().
const conformingData = jt.toQuads(BookSchema, { 'title': 'Die unendliche Geschichte' });
const conformingReport: ShaclValidationReportType = jt.validateWithShacl(shapes, conformingData);
console.assert(
conformingReport.conforms,
'conforming: report.conforms must be true'
);
console.assert(
conformingReport.results.length === 0,
'conforming: results array must be empty'
);
console.log('Conforming report.conforms :', conformingReport.conforms);
console.log('Conforming results.length :', conformingReport.results.length);
// ── Case 2: NON-CONFORMING instance (missing required title) ──────────────
// toQuads() validates the data first, so we hand-craft quads that omit title.
// The predicate IRI must match the sh:path in the emitted shapes — both are
// produced by the same predicateResolver, so they are always in sync.
// We discover the IRI from the shapes rather than hard-coding it.
const shapeQuads = shapes.shaclQuads();
const titlePathQuad = shapeQuads.find((quad) => {
return quad.predicate.value === SH.path && quad.object.value.endsWith('/title');
});
if (titlePathQuad === undefined) {
throw new Error('sh:path for title not found in shape quads — shape projection is broken');
}
const titlePath = titlePathQuad.object.value;
const focusNode = `${BASE}/instances/book-no-title`;
// A Book node with rdf:type but no title predicate.
function makeTypeQuad(subject: string, classIri: string): QuadInterface {
return {
'equals': () => {
return false;
},
'graph': Terms.defaultGraph(),
'object': Terms.iri(classIri),
'predicate': Terms.iri(RDF.type),
'subject': Terms.iri(subject),
'termType': 'Quad',
'value': ''
};
}
// Title absent — violates sh:minCount 1 on the title property shape.
const nonConformingData: QuadInterface[] = [makeTypeQuad(focusNode, `${BASE}/Book`)];
const nonConformingReport: ShaclValidationReportType = jt.validateWithShacl(shapes, nonConformingData);
console.assert(
!nonConformingReport.conforms,
'non-conforming: report.conforms must be false'
);
console.assert(
nonConformingReport.results.length > 0,
'non-conforming: results array must be non-empty'
);
const violation = nonConformingReport.results.find((result) => {
return result.sourceConstraintComponent === SH.MinCountConstraintComponent;
});
console.assert(
violation !== undefined,
'non-conforming: MinCountConstraintComponent violation expected for missing title'
);
console.assert(
violation?.focusNode === focusNode,
'non-conforming: focusNode matches the hand-crafted subject IRI'
);
console.assert(
violation?.resultPath === titlePath,
'non-conforming: resultPath matches the title sh:path IRI'
);
console.assert(
violation?.resultSeverity === 'Violation',
'non-conforming: resultSeverity is Violation'
);
console.log('\nNon-conforming report.conforms :', nonConformingReport.conforms);
console.log('Non-conforming results.length :', nonConformingReport.results.length);
if (violation !== undefined) {
console.log('\nViolation:');
console.log(' focusNode :', violation.focusNode);
console.log(' resultPath :', violation.resultPath);
console.log(' resultSeverity :', violation.resultSeverity);
console.log(' sourceConstraintComponent:', violation.sourceConstraintComponent);
console.log(' resultMessage :', violation.resultMessage);
}
// ── Case 3: Conforming instance with author ───────────────────────────────
// Round-trip through toQuads() with optional author filled in.
const fullBookData = jt.toQuads(BookSchema, {
'author': { 'name': 'Michael Ende' },
'title': 'Die unendliche Geschichte'
});
const fullBookReport: ShaclValidationReportType = jt.validateWithShacl(shapes, fullBookData);
console.assert(
fullBookReport.conforms,
'full book with author: report.conforms must be true'
);
console.log('\nFull book with author — conforms:', fullBookReport.conforms);
// ── Case 4: Non-conforming — multiple violations ───────────────────────────
// A node typed as Author that is also missing its required `name` property
// will trigger a MinCountConstraintComponent on the name path.
const shapeQuadsForLookup = shapes.shaclQuads();
const namePathQuad = shapeQuadsForLookup.find((quad) => {
return quad.predicate.value === SH.path && quad.object.value.endsWith('/name');
});
if (namePathQuad === undefined) {
throw new Error('sh:path for name not found in shape quads — shape projection is broken');
}
const namePath = namePathQuad.object.value;
const noNameFocusNode = `${BASE}/instances/author-no-name`;
// An Author node with rdf:type but no name predicate.
const noNameData: QuadInterface[] = [makeTypeQuad(noNameFocusNode, `${BASE}/Author`)];
const noNameReport: ShaclValidationReportType = jt.validateWithShacl(shapes, noNameData);
console.assert(
!noNameReport.conforms,
'no-name author: report.conforms must be false (name is required)'
);
const nameViolation = noNameReport.results.find((result) => {
return result.sourceConstraintComponent === SH.MinCountConstraintComponent
&& result.resultPath === namePath;
});
console.assert(
nameViolation !== undefined,
'no-name author: MinCountConstraintComponent violation expected for missing name'
);
console.assert(
nameViolation?.resultSeverity === 'Violation',
'no-name author: resultSeverity is Violation'
);
console.log('\nNo-name author report.conforms :', noNameReport.conforms);
console.log('No-name author violations.length:', noNameReport.results.length);
Example 2: Accessing raw SHACL shape quads
/**
* OntologyBuilder.shaclQuads — raw SHACL shape quad array.
*
* `toShacl()` returns an `OntologyBuilder` whose SHACL quad store is
* populated by the SHACL serializer. `shaclQuads()` exposes those quads
* directly as an `QuadInterface[]`, giving consumers a quad-level workaround
* surface for post-processing, filtering, or feeding into custom RDF pipelines.
*
* The OWL quad store (`quads()`) is empty for a `toShacl()` builder — SHACL
* content lives exclusively in `shaclQuads()`.
*/
import { bookstoreEntities } from '../bookstore/index.js';
import {
RDF, SH
} from '../../../src/constants/IRI.js';
const shaclBuilder = bookstoreEntities.toShacl();
// shaclQuads() returns the raw SHACL shape quads from the SHACL store.
const shQuads = shaclBuilder.shaclQuads();
console.assert(
shQuads.length > 0,
'shaclQuads() must return at least one quad'
);
// Every SHACL node shape is expressed as an rdf:type sh:NodeShape triple.
const hasNodeShape = shQuads.some((quad) => {
return (
quad.predicate.value === RDF.type
&& quad.object.value === SH.NodeShape
);
});
console.assert(
hasNodeShape,
'shaclQuads() must contain at least one rdf:type sh:NodeShape quad'
);
// The OWL quad store is empty for a toShacl() builder.
const owlQuads = shaclBuilder.quads();
console.assert(
owlQuads.length === 0,
'quads() must be empty for a toShacl() builder'
);
console.log('shaclQuads() count:', shQuads.length);
console.log('contains sh:NodeShape typed quad:', hasNodeShape);
console.log('quads() length (must be 0):', owlQuads.length);
// Sample the first sh:NodeShape subject IRI for visibility.
const nodeShapeQuad = shQuads.find((quad) => {
return quad.predicate.value === RDF.type && quad.object.value === SH.NodeShape;
});
if (nodeShapeQuad !== undefined) {
console.log('first sh:NodeShape subject:', nodeShapeQuad.subject.value);
}
Workflow
jt.toShacl() → OntologyBuilder (shapes)
jt.toQuads(schema, v) → QuadInterface[] (data)
jt.validateWithShacl(shapes, data) → ShaclValidationReportInterfaceBuild shapes once at startup and reuse them across validation calls. The toShacl() builder is not cached (see toShacl()); hold the returned OntologyBuilder in a variable when validating in a loop.
Related
toShacl()— emit the SHACL shapestoQuads()— produce ABox data quadsontology()— combined TBox + SHACL builder- RDF round-trip — full quad projection and lifting
See also
- Bookstore domain — schemas used in examples
- Graph concepts — TBox / ABox structure