Library API¶
import {
validate, createValidator, formatReport, groupIssues,
} from '@theodi/data-standard-validator'
The package is ESM-only and needs Node.js 22+ or a modern bundler. Node loads a build that can read file paths. Browsers and bundlers load a build without any filesystem code. Both have the same API.
validate(options): one call¶
const report = await validate({
shapes: 'shapes/person.ttl',
data: ['a.json', 'b.json'],
})
This loads the shapes, checks every document, runs any cross-checks across the
batch, and resolves to a RunReport.
createValidator(options): load once, validate many¶
Parsing large shapes files is the slow part. Hold on to a validator to pay for it once:
const validator = await createValidator({ shapes: 'shapes/person.ttl', context: 'context.jsonld' })
const one = await validator.validate({ name: 'upload.json', text }) // DocumentReport
const all = await validator.validateAll(['a.json', 'b.json']) // RunReport
validator.setup // { shapes, context?, warnings } - what it checks against
validator.skolemSafe // false if a shape needs real blank nodes (see how-it-works.md)
createValidator rejects with a SourceError if a shapes file or the context
cannot be loaded or parsed. Problems with a document do not reject. Invalid
JSON or an unloadable @context becomes an issue in the report.
Options¶
| Option | Type | Description |
|---|---|---|
shapes |
Source \| Source[] |
Required. SHACL shapes in Turtle. Several are merged, in order. |
context |
Source |
A JSON-LD context that replaces each document's @context. The file may be written as {"@context": {...}} or as the bare context. |
data |
Source \| Source[] |
validate() only: the documents to check. |
crossChecks |
CrossCheck[] |
Rules across the whole batch (see below). |
patterns |
PatternHint[] |
Plain-English names for the sh:pattern regexes your shapes use. |
skolemize |
boolean |
Default true. Set to false to stop tracing results back to JSON paths. It is turned off automatically when a shape requires blank nodes. |
fetch |
typeof fetch |
Used for URL sources. Defaults to the global fetch. Inject one for tests, auth headers or caching. |
readFile |
(path) => Promise<string> |
Used for file paths. The Node build supplies one. In a browser, path sources fail unless you pass one. |
Sources¶
Every input (shapes, context, documents) is a Source:
type Source =
| string // URL if it has a scheme, else a file path
| { url: string, optional?: boolean, label?: string }
| { path: string, optional?: boolean, label?: string }
| { text: string, name?: string, base?: string } // content you already have
| { json: unknown, name?: string, base?: string } // already-parsed JSON
nameis what the report calls the document. A URL or path names itself.baseis where relative references inside inline content resolve from, such as"@context": "./context.jsonld".optionalapplies to shapes. If an optional shapes file is not found, a warning is added toreport.setup.warningsand validation continues without it.labelcompletes that warning ("the conditional rules could not be loaded").- Prefer
{ text }over{ json }for documents when you have the text. Only the text gives issues line and column numbers.
Formatting a report¶
formatReport(report, 'text', { color: false, sources }) // the CLI's output
formatReport(report, 'json') // JSON.stringify, pretty-printed
formatReport(report, 'markdown', { title: 'People data' })
| Option | Applies to | Description |
|---|---|---|
color |
text | ANSI colours. Default true. |
sources |
text | Map<documentName, text>, used to print the offending line under each issue. |
title |
text, markdown | Heading. Defaults to the shapes' file names. |
renderText, renderJson and renderMarkdown are exported too.
To render issues yourself, for example in a web page,
groupIssues(document.issues) groups them by the object they belong to, the way
the built-in formats do. Each group has a label such as Address (address[0]).
Issue titles and hints use two bits of inline markup: `code` and
**bold**.
Pattern hints¶
A regex is hard to describe in words, so the library does not try. When an
sh:pattern fails, the hint is built from the shape's sh:description, using
any (e.g. ...) in it as the example. To say it better, name your patterns:
const validator = await createValidator({
shapes,
patterns: [
{ pattern: '^[0-9]{10}$', description: 'a 10-digit NHS number', example: '9434765919' },
{ pattern: /^\^\[A-Z\]\{1,2\}/, description: 'a UK postcode in upper case', example: 'AB1 2CD' },
],
})
A string matches the shape's regex source exactly. A RegExp is tested
against that source.
Cross-checks¶
SHACL validates one node at a time, so it cannot express a rule like "no two
documents share an identifier". A cross-check runs after SHACL, over the RDF of
every document in a validateAll batch:
import { namedNode, type CrossCheck } from '@theodi/data-standard-validator'
const uniqueId: CrossCheck = {
id: 'unique-id',
title: 'the same identifier appears in several documents',
run (documents) {
const seen = new Map<string, string[]>()
for (const { name, dataset } of documents) {
for (const quad of dataset.match(null, namedNode('https://example.org/identifier'), null)) {
seen.set(quad.object.value, [...(seen.get(quad.object.value) ?? []), name])
}
}
const findings = [...seen].filter(([, docs]) => docs.length > 1)
.map(([id, docs]) => ({ message: `${id} appears in ${docs.join(', ')}`, documents: docs }))
return { ok: findings.length === 0, findings }
},
}
await validate({ shapes, data: files, crossChecks: [uniqueId] })
dataset is an RDF/JS DatasetCore. A
failed check makes report.conforms false and appears in report.crossChecks.
Errors¶
| Class | When |
|---|---|
SourceError |
A source could not be loaded or parsed. error.source names it, and error.status holds the HTTP status if there was one. |
NotFoundError |
A subclass of SourceError: HTTP 404, or a missing file. |
In the browser¶
import { createValidator } from '@theodi/data-standard-validator'
const validator = await createValidator({ shapes: 'https://example.org/shape.ttl' })
const report = await validator.validateAll([{ name: 'pasted', text: textarea.value }])
- Shapes and contexts must be served with CORS headers. Files on
raw.githubusercontent.comare. - Parsing large shapes can take a noticeable moment, so run validation in a Web Worker to keep the page responsive.
rdf-ext, one of the RDF libraries underneath, expects a globalwindow. Inside a worker, define it before the library loads. Put this in a module imported first:
ts
// worker-globals.ts
;(globalThis as { window?: unknown }).window ??= globalThis
TypeScript¶
Types ship with the package. The exported types are Source,
ValidatorOptions, ValidateOptions, Validator, RunReport,
DocumentReport, Issue, IssueCode, Location, Severity,
CrossCheck, CrossCheckDocument, CrossCheckReport, PatternHint,
Format, FormatOptions and IssueGroup.