Skip to content

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

ts
jt.validateWithShacl(
  shapes: OntologyBuilder | readonly QuadInterface[],
  data:   readonly QuadInterface[]
): ShaclValidationReportInterface

shapes: 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:

FieldTypeDescription
conformsbooleantrue when all constraints pass
resultsShaclValidationResultInterface[]One entry per constraint violation

ShaclValidationResultInterface

Each violation entry in report.results carries:

FieldTypeDescription
focusNodestringIRI of the node that failed the shape constraint
resultPathstringPredicate IRI of the property that failed
resultSeverity'Violation' | 'Warning' | 'Info'SHACL severity level
sourceConstraintComponentstringIRI of the constraint component that fired
valueunknownThe offending value, when available
resultMessagestringHuman-readable description of the violation

Constraint components covered

validateWithShacl evaluates all SHACL shapes produced by toShacl(). The shape set includes:

ComponentSHACL IRITriggered by
sh:MinCountConstraintComponentsh:minCountRequired properties absent
sh:MaxCountConstraintComponentsh:maxCountProperties exceeding cardinality
sh:DatatypeConstraintComponentsh:datatypeLiteral value with wrong XSD type
sh:MinLengthConstraintComponentsh:minLengthString shorter than minLength
sh:MaxLengthConstraintComponentsh:maxLengthString longer than maxLength
sh:PatternConstraintComponentsh:patternString not matching pattern
sh:MinInclusiveConstraintComponentsh:minInclusiveNumber below minimum
sh:MaxInclusiveConstraintComponentsh:maxInclusiveNumber above maximum
sh:MinExclusiveConstraintComponentsh:minExclusiveNumber not above exclusiveMinimum
sh:MaxExclusiveConstraintComponentsh:maxExclusiveNumber not below exclusiveMaximum
sh:NodeKindConstraintComponentsh:nodeKindSubject is wrong node kind (IRI vs literal)
sh:ClassConstraintComponentsh:classObject does not have the expected rdf:type

Bounded scope

validateWithShacl validates shapes produced by toShacl() and ShaclProjection. It:

  • Uses implicit class targeting: each sh:NodeShape is matched to subjects whose rdf:type quad names the corresponding class IRI. Focus nodes without a matching rdf:type are not evaluated.
  • Evaluates only the property constraints encoded in the shapes — structural JSON Schema constraints that ShaclProjection maps 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);
Output
Press Execute to run this example against the real library.

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);
}
Output
Press Execute to run this example against the real library.

Workflow

jt.toShacl()           →  OntologyBuilder (shapes)
jt.toQuads(schema, v)  →  QuadInterface[] (data)
jt.validateWithShacl(shapes, data)  →  ShaclValidationReportInterface

Build 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.


See also

Released under the MIT License.