OWL 2 TBox import (fromTbox) v0.10.0+
fromTbox is the inverse of toTbox. It reads an OWL 2 TBox (as JSON-LD, a quad array, or an N-Quads string) and reconstructs JSON Schema objects for every declared class, along with invariants, property characteristics, owl:sameAs pairs, and named individuals.
The round-trip contract is:
fromTbox ∘ toTbox ≈ identity (on the supported OWL 2 axiom set)"Approximately identity" because the OWL TBox encodes a subset of JSON Schema semantics. Primitive type facets (minLength, minimum, format, etc.) are not OWL class axioms and are not carried back through a generic OWL round-trip. The contract holds for all axioms listed in the Supported axioms table below.
API
Static helper (no registry state)
import { JsonTology } from 'json-tology';
const result: OwlImportResult = JsonTology.fromTbox(
jsonLdStringOrQuads, // string | object | QuadInterface[]
{ baseIri?: string; prefixes?: Record<string, string> }
);The static helper is stateless: it constructs a transient OwlImporter and discards it. Use it when you only need the reconstructed schemas as plain objects.
Instance method (registers into the live registry)
const jt = JsonTology.create({ baseIri: 'https://example.org' });
const result: OwlImportResult = jt.fromTbox(
jsonLdStringOrQuads,
{ register?: boolean } // default: true
);When register: true (the default), all produced schemas are passed to registry.set(), invariants are registered, sameAs pairs are applied, and property characteristics are recorded. This makes the imported vocabulary immediately available for validate() / instantiate() / materialize() calls.
Return type: OwlImportResult
| Field | Type | Description |
|---|---|---|
schemas | JsonSchemaDocumentObjectType[] | One schema per declared class IRI |
invariants | { invariant, schemaId }[] | Structural invariants from OWL axioms |
characteristics | { characteristic, propertyIri }[] | OWL property characteristics |
sameAs | [string, string][] | owl:sameAs identity pairs |
individuals | { iri, types, properties }[] | Named individuals (ABox) |
unsupported | { axiomIri, subjectIri }[] | Axioms no dispatcher handled |
Supported axioms
| OWL 2 axiom | Serialised predicate IRI | JSON Schema mapping |
|---|---|---|
| Class declaration | rdf:type owl:Class | { $id: classIri, type: 'object' } stub |
rdfs:subClassOf | rdfs:subClassOf | allOf: [{ $ref: parentIri }] |
owl:equivalentClass | owl:equivalentClass | $ref: equivalentIri |
owl:disjointWith | owl:disjointWith | disjointWith: otherIri (symmetric) |
owl:complementOf | owl:complementOf | not: { $ref: complementIri } |
owl:disjointUnionOf | owl:disjointUnionOf | oneOf on member IRIs |
owl:ObjectProperty with domain+range | rdfs:domain / rdfs:range | properties[name]: { $ref: rangeIri } |
owl:DatatypeProperty with xsd:string range | rdfs:domain / rdfs:range | properties[name]: { type: 'string' } |
owl:DatatypeProperty with xsd:boolean range | rdfs:domain / rdfs:range | properties[name]: { type: 'boolean' } |
owl:DatatypeProperty with xsd:integer / xsd:int range | rdfs:domain / rdfs:range | properties[name]: { type: 'integer' } |
owl:DatatypeProperty with xsd:decimal / xsd:double range | rdfs:domain / rdfs:range | properties[name]: { type: 'number' } |
owl:oneOf datatype enumeration | owl:oneOf / rdf:rest / rdf:first | enum: [...] |
| Property characteristics | rdf:type owl:FunctionalProperty etc. | Recorded in characteristics |
owl:NamedIndividual | rdf:type owl:NamedIndividual | Recorded in individuals |
owl:sameAs | owl:sameAs | Recorded in sameAs |
rdfs:subPropertyOf | rdfs:subPropertyOf | Recorded in characteristics |
Example
/**
* OWL Import round-trip — `fromTbox ∘ toTbox` contract.
*
* Demonstrates the full import pipeline:
* 1. Export the bookstore TBox as JSON-LD (`jt.toTbox().jsonLd()`).
* 2. Re-import it into a fresh registry (`jt.fromTbox()`).
* 3. Verify the OWL class-axiom structure round-trips correctly.
* 4. Validate a Bastian-orders-Neverending ABox instance against the
* imported `Book` schema (fields whose types survive the OWL round-trip).
*
* The round-trip preserves OWL 2 class axioms:
* - `rdfs:subClassOf` → `allOf: [{ $ref }]`
* - `owl:complementOf` → `not: { $ref }`
* - `owl:disjointWith` → `disjointWith`
* - `owl:equivalentClass` → `$ref`
* - Property domain + range → `properties` with `type: 'string'` / `$ref`
*
* What the OWL round-trip does NOT preserve:
* XSD facets (minLength, minimum, pattern, format) — these are literal
* range annotations on the property, not OWL class axioms. Primitive
* schemas like `PersonName` (type: string, minLength: 1) emerge from
* fromTbox as `type: object` (OWL class with no scalar type). Use the
* original registry for full ABox validation; use the imported registry
* for structural/graph queries and OWL-level reasoning.
*/
import { JsonTology } from '../../../src/index.js';
import { bookstoreEntities } from '../bookstore/index.js';
// Step 1: export the canonical bookstore TBox as a JSON-LD string.
const tboxJsonLd = bookstoreEntities.toTbox().jsonLd();
// Step 2: re-import the TBox into a fresh registry using the instance method.
// `register: true` (the default) registers all produced schemas so the registry
// can resolve cross-schema $refs during validation.
const freshJt = JsonTology.create({
'baseIri': 'https://bookstore.example',
'enableStrictGraph': false
});
const result = freshJt.fromTbox(tboxJsonLd);
// → 0 after Phase 1: all eight dispatchers handle every bookstore axiom.
console.log(`Imported ${result.schemas.length} schemas from bookstore TBox`);
console.log(`Unsupported axioms: ${result.unsupported.length}`);
// Step 3: verify OWL class-axiom structure.
// 3a. subClassOf — RareBook → PrintBook
const rareBookSchema = result.schemas.find((schema) => {
return schema.$id === 'urn:bookstore:RareBook';
}) as Record<string, unknown> | undefined;
const rareBookAllOf = rareBookSchema?.allOf as Array<Record<string, unknown>> | undefined;
const inheritsFromPrintBook = Array.isArray(rareBookAllOf)
&& rareBookAllOf.some((entry) => {
return entry.$ref === 'urn:bookstore:PrintBook';
});
console.assert(inheritsFromPrintBook, 'subClassOf preserved: RareBook → PrintBook');
// Prints: true
console.log('subClassOf (RareBook → PrintBook):', inheritsFromPrintBook);
// 3b. complementOf — OutOfPrintBook = ¬InPrintBook
const outOfPrintSchema = result.schemas.find((schema) => {
return schema.$id === 'urn:bookstore:OutOfPrintBook';
}) as Record<string, unknown> | undefined;
const outOfPrintNot = outOfPrintSchema?.not as Record<string, unknown> | undefined;
console.assert(
outOfPrintNot?.$ref === 'urn:bookstore:InPrintBook',
'complementOf preserved: OutOfPrintBook ¬InPrintBook'
);
// Prints: true
console.log('complementOf (OutOfPrintBook ¬ InPrintBook):', outOfPrintNot?.$ref === 'urn:bookstore:InPrintBook');
// 3c. equivalentClass — AuthorName ≡ PersonName
const authorNameSchema = result.schemas.find((schema) => {
return schema.$id === 'urn:bookstore:AuthorName';
}) as Record<string, unknown> | undefined;
console.assert(
authorNameSchema?.$ref === 'urn:bookstore:PersonName',
'equivalentClass preserved: AuthorName ≡ PersonName'
);
// Prints: true
console.log('equivalentClass (AuthorName ≡ PersonName):', authorNameSchema?.$ref === 'urn:bookstore:PersonName');
// Step 4: validate a Bastian ABox snippet against the imported Book schema.
//
// The imported Book schema preserves `inStock: boolean` (owl:DatatypeProperty
// with xsd:boolean range) and all `$ref`-typed object properties. Only scalar
// string/number fields without $ref ranges lose their facets.
const bookSchema = result.schemas.find((schema) => {
return schema.$id === 'urn:bookstore:Book';
});
console.assert(bookSchema !== undefined, 'Book schema must be present after round-trip');
// Bastian's rare-book stub — uses only fields that survive the OWL round-trip.
// `inStock` is preserved as `type: boolean` (xsd:boolean DatatypeProperty range).
// The title/isbn fields have no facet constraints in the imported schema so they
// pass without structural constraints in the OWL-imported registry.
const neverendingStub = { 'inStock': true };
const errs = freshJt.validate(
bookSchema as Record<string, unknown> & { '$id': string },
neverendingStub
);
console.assert(errs.ok, `Book stub should validate against imported schema; errors: ${JSON.stringify(errs)}`);
// Prints: true
console.log('Bastian neverending stub validates against imported Book schema:', errs.ok);
Limitations
The following OWL 2 constructs are not yet mapped to JSON Schema constraints:
| OWL 2 construct | Axiom IRI | Status |
|---|---|---|
Qualified cardinality (owl:minQualifiedCardinality, owl:maxQualifiedCardinality) | owl:onClass | Not mapped to minItems / maxItems |
Anonymous restriction nodes (owl:Restriction + owl:someValuesFrom / owl:allValuesFrom) outside the json-tology serialiser output | owl:Restriction | Reconstructed as jt:restrictions from json-tology TBox; not generalised for arbitrary OWL serialisations |
owl:intersectionOf on class expressions | owl:intersectionOf | Partially handled via allOf; complex nested class expressions are logged as unsupported |
owl:unionOf on class expressions | owl:unionOf | Partially handled via oneOf; complex anonymous class union nodes are logged as unsupported |
rdfs:comment / rdfs:label literal annotations | rdfs:comment, rdfs:label | Not carried into title or description (annotation triples are consumed but not mapped) |
Datatype facets (XSD owl:withRestrictions) | owl:withRestrictions | Not mapped to minLength / minimum / pattern |
owl:hasValue restrictions | owl:hasValue | Not implemented |
owl:hasSelf restrictions | owl:hasSelf | Not implemented |
If the input contains any of the above, the affected axiom IRI appears in result.unsupported. Round-tripping a TBox that was exported by json-tology's own toTbox() will produce zero unsupported entries because toTbox() only emits axioms in the supported set.
Round-trip fidelity
Run fromTbox(toTbox(schemas).jsonLd()) against the canonical bookstore registry to confirm the supported axiom set round-trips cleanly. The example in the previous section demonstrates this: it exports the bookstore TBox, reimports it into a fresh registry, and asserts that every OWL class axiom (subClassOf, disjointWith, complementOf, equivalentClass, property domain + range) is reconstructed correctly. The full source is at examples/docs/advanced/90-owl-import-roundtrip.ts.
Compile-time types via codegen
JsonTology.fromTbox (and the OwlImporter underneath it) resolve an OWL TBox at runtime. TypeScript's type system operates at compile time: it cannot reach into an external file, execute it, and derive types from the result. This means that even though fromTbox reconstructs accurate JSON Schema objects, the static type of those schemas is JsonSchemaDocumentObjectType, not a narrow as const literal from which InferType<…> can extract a meaningful compile-time type.
The solution is a build-step code generator: run the ontology through a code generator once, write the resulting TypeScript module to disk, and then import that module as ordinary source. The generated module contains as const schema literals identical to what you would write by hand, so InferType<typeof PersonSchema> works exactly as it does for hand-authored schemas. The generator also emits a SchemaReferencesMapType over the full schema set and threads it into every per-class InferType, so cross-class $refs (e.g. Book.author → Person) resolve to the precise sibling type rather than unknown — the ontology → TypeScript direction round-trips losslessly.
Codegen workflow
ontology JSON-LD → json-tology owl-gen → TypeScript source → consumer imports- Input: any JSON-LD string or file that
fromTboxaccepts. - Generator: the
owl-gensubcommand (or thegenerateFromTboxprogrammatic API). - Output: a
.tsmodule that exports oneas constschema literal per OWL class, with a matchingexport typeper class derived viaInferType. - Consumption: import the generated module in your application the same way you import hand-authored schemas.
CLI
npx json-tology owl-gen ./foaf.jsonld --out ./src/generated/foaf.tsOptions:
| Flag | Default | Description |
|---|---|---|
--out <path> | stdout | Write the generated TypeScript source to <path>. |
--name <id> | input filename basename (sanitized) | Identifier prefix used for namespace exports. |
--base-iri <iri> | (none) | Override baseIri passed to fromTbox. |
Programmatic API
import { generateFromTbox } from 'json-tology/owl-gen';
const source: string = generateFromTbox({
input: jsonLdStringOrObject, // string | object
name?: string, // identifier prefix (e.g. 'foaf')
baseIri?: string, // passed through to fromTbox
});generateFromTbox returns the generated TypeScript source as a string. Writing it to disk is the caller's responsibility: use fs.writeFileSync or pass --out on the CLI.
Runnable example
/**
* OWL codegen round-trip — what a generated module looks like.
*
* The `json-tology owl-gen` CLI and the `generateFromTbox` programmatic API
* accept any OWL 2 TBox that `fromTbox` can read and emit a TypeScript source
* file containing `as const` schema literals. Because those literals are
* ordinary TypeScript constants, `InferType<typeof Schema>` extracts a
* compile-time type just like a hand-authored schema.
*
* This file demonstrates the full round-trip in a single runnable script:
*
* 1. Define a small synthetic 2-class ontology inline as JSON-LD
* (foaf:Person and foaf:Group, Neverending-Story flavoured).
* 2. Call `generateFromTbox` to produce the TypeScript source string.
* 3. Assert the generated source contains expected export declarations.
* 4. Use a locally-authored `InferType` annotation to show compile-time
* type derivation from the same schema shape.
*
* Browser-safe: no node:fs, node:path, or node:url. The source string is
* inspected in-memory; no disk writes or dynamic imports are performed.
*/
import type { InferType } from '../../../src/types/index.js';
import { JsonTology } from '../../../src/index.js';
import { generateFromTbox } from '../../../src/owl-gen/index.js';
import { bookstoreEntities } from '../bookstore/index.js';
// ---------------------------------------------------------------------------
// Inline synthetic ontology — foaf-style, Neverending-Story characters
// ---------------------------------------------------------------------------
const syntheticTboxJsonLd = JSON.stringify({
'@context': {
'ex': 'https://neverending.example/',
'owl': 'http://www.w3.org/2002/07/owl#',
'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
'rdfs': 'http://www.w3.org/2000/01/rdf-schema#',
'xsd': 'http://www.w3.org/2001/XMLSchema#'
},
'@graph': [
// foaf:Person analogue — a character in the story
{
'@id': 'https://neverending.example/Person',
'@type': 'owl:Class'
},
// foaf:name
{
'@id': 'https://neverending.example/name',
'@type': 'owl:DatatypeProperty',
'rdfs:domain': { '@id': 'https://neverending.example/Person' },
'rdfs:range': { '@id': 'xsd:string' }
},
// foaf:Group analogue — a faction or realm in Fantastica
{
'@id': 'https://neverending.example/Group',
'@type': 'owl:Class'
},
// foaf:member — Group has members who are Persons
{
'@id': 'https://neverending.example/member',
'@type': 'owl:ObjectProperty',
'rdfs:domain': { '@id': 'https://neverending.example/Group' },
'rdfs:range': { '@id': 'https://neverending.example/Person' }
}
]
});
// ---------------------------------------------------------------------------
// Step 1: generate TypeScript source from the inline TBox
// ---------------------------------------------------------------------------
const generatedSrc = generateFromTbox({
'input': syntheticTboxJsonLd,
'name': 'neverending'
});
// ---------------------------------------------------------------------------
// Step 2: verify the generated source structure
// ---------------------------------------------------------------------------
console.assert(
generatedSrc.includes('export const PersonSchema'),
'Generated source must export PersonSchema'
);
console.assert(
generatedSrc.includes('export const GroupSchema'),
'Generated source must export GroupSchema'
);
console.assert(
generatedSrc.includes('as const'),
'Generated source must use as const literals'
);
console.assert(
generatedSrc.includes('InferType'),
'Generated source must re-export InferType-derived types'
);
console.log('generateFromTbox source check passed — all expected exports present.');
console.log('Generated source length:', generatedSrc.length);
console.log('Contains PersonSchema export:', generatedSrc.includes('export const PersonSchema'));
console.log('Contains GroupSchema export:', generatedSrc.includes('export const GroupSchema'));
// ---------------------------------------------------------------------------
// Step 3: validate a Neverending-Story fixture against the runtime-imported schema
// ---------------------------------------------------------------------------
// Use the runtime fromTbox path to validate (no disk I/O needed).
const jt = JsonTology.create({
'baseIri': 'https://neverending.example/',
'enableStrictGraph': false
});
const result = jt.fromTbox(syntheticTboxJsonLd);
const PersonSchema = result.schemas.find((schema) => {
return schema.$id === 'https://neverending.example/Person';
});
console.assert(PersonSchema !== undefined, 'Person schema must be present after fromTbox');
if (PersonSchema !== undefined && typeof PersonSchema.$id === 'string') {
const bastian = { 'name': 'Bastian Balthazar Bux' };
const validationResult = jt.validate(
PersonSchema as Record<string, unknown> & { '$id': string },
bastian
);
console.assert(
validationResult.ok,
`Bastian fixture must validate; errors: ${JSON.stringify(validationResult)}`
);
console.log('Bastian validates against runtime-imported PersonSchema:', validationResult.ok);
}
// ---------------------------------------------------------------------------
// Step 4: compile-time type demonstration — InferType on a schema shape
// ---------------------------------------------------------------------------
// Because `generateFromTbox` emits `as const` literals, `InferType<…>` works
// correctly. We annotate with a locally-authored shape that mirrors the output;
// in a real consumer this comes directly from the generated module's exports.
type GeneratedPerson = InferType<{
readonly '$id': 'https://neverending.example/Person';
readonly 'properties': {
readonly 'name': { readonly 'type': 'string' };
};
readonly 'type': 'object';
}>;
const cornelia: GeneratedPerson = { 'name': 'Cornelia Funke' };
console.assert(
typeof cornelia.name === 'string',
'Cornelia Funke fixture must satisfy GeneratedPerson compile-time type'
);
console.log('Cornelia Funke type-check passes:', typeof cornelia.name === 'string');
// ---------------------------------------------------------------------------
// Bonus: bookstore TBox — show what a larger codegen input looks like
// ---------------------------------------------------------------------------
const bookstoreTboxJsonLd = bookstoreEntities.toTbox().jsonLd();
const bookstoreImport = JsonTology.fromTbox(bookstoreTboxJsonLd);
console.assert(
bookstoreImport.schemas.length > 0,
'Bookstore TBox must produce at least one schema'
);
console.assert(
bookstoreImport.unsupported.length === 0,
`Bookstore TBox must have zero unsupported axioms; got: ${bookstoreImport.unsupported.length}`
);
console.log(`Bookstore TBox: ${bookstoreImport.schemas.length} schemas, ${bookstoreImport.unsupported.length} unsupported`);
Node.js disk I/O — writeFromTbox / writeRegistryDirectory
The json-tology/owl-gen-node entry adds disk I/O over the browser-safe owl-gen core. writeFromTbox writes a single TypeScript source file; writeRegistryDirectory writes entities/<Name>.ts per OWL class plus an index.ts that constructs the full registry.
/**
* owl-gen-node — writeFromTbox and writeRegistryDirectory.
*
* The Node-only `owl-gen-node` entry adds disk I/O over the browser-safe
* `owl-gen` core. `writeFromTbox` writes a single TypeScript source file;
* `writeRegistryDirectory` writes `entities/<Name>.ts` per OWL class plus
* an `index.ts` that constructs the full registry.
*
* This example writes to a unique temp directory under `os.tmpdir()` and
* removes it on completion. No files are left in the repository.
*/
import {
existsSync, mkdirSync, rmSync
} from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import {
writeFromTbox, writeRegistryDirectory
} from '../../../src/owl-gen-node/index.js';
import { foafSubset } from '../ontologies/foaf-subset.js';
// Unique temp directory — isolated per run, removed on completion.
const tempDir = join(tmpdir(), `jt-example-123-${Date.now()}`);
mkdirSync(tempDir, { 'recursive': true });
try {
// ── writeFromTbox ────────────────────────────────────────────────────────
// Writes a single TypeScript source file containing all OWL class schemas
// derived from the TBox. The generated file exports `as const` schema
// literals and re-exports `InferType`-derived types.
const singleOutput = join(tempDir, 'foaf-schemas.ts');
writeFromTbox({
'input': JSON.stringify(foafSubset),
'name': 'foaf',
'output': singleOutput
});
console.assert(
existsSync(singleOutput),
'writeFromTbox must write the output file to disk'
);
console.log('writeFromTbox wrote:', singleOutput);
// ── writeRegistryDirectory ───────────────────────────────────────────────
// Writes `entities/<Name>.ts` per OWL class and `index.ts` that constructs
// the full registry. Returns absolute paths for all written files.
const dirOutput = join(tempDir, 'foaf-dir');
const result = writeRegistryDirectory({
'input': JSON.stringify(foafSubset),
'name': 'foaf',
'outDir': dirOutput
});
console.assert(
result.entityFiles.length > 0,
'writeRegistryDirectory must write at least one entity file'
);
const indexExists = existsSync(result.indexFile);
console.assert(
indexExists,
'writeRegistryDirectory must write an index.ts file'
);
for (const entityFile of result.entityFiles) {
console.assert(
existsSync(entityFile.path),
`entity file must exist on disk: ${entityFile.path}`
);
}
console.log('writeRegistryDirectory entity files:', result.entityFiles.length);
console.log('index.ts exists:', indexExists);
console.log(
'entity names:',
result.entityFiles.map((entityFile) => {
return entityFile.name;
}).join(', ')
);
} finally {
// Always remove the temp directory — leaves no repo files.
rmSync(tempDir, {
'force': true,
'recursive': true
});
console.log('temp directory removed (clean)');
}
Build-time integration
package.json prebuild hook. Run the generator before every TypeScript compilation so the generated file is always current before tsc or your bundler starts:
{
"scripts": {
"gen:foaf": "json-tology owl-gen ./ontologies/foaf.jsonld --out ./src/generated/foaf.ts",
"prebuild": "npm run gen:foaf"
}
}This is the simplest option. Every npm run build re-generates the file first. CI receives the generated file baked into the source tree (commit it); or generate-on-CI and exclude it from the repo. Either pattern works.
Vite plugin. Wrap generateFromTbox in a Vite plugin's buildStart hook to integrate with the dev-server watch cycle. The plugin calls generateFromTbox, writes the result to disk, and invalidates the dependent module so HMR re-processes consumers. This is appropriate when the source ontology lives in public/ or arrives via a remote URL that changes during development.
Husky pre-commit hook. Add a pre-commit script that regenerates all .ts outputs and git adds them. If the generated file changed, the commit captures the update automatically. This is a lightweight "always fresh" guarantee that does not require separate CI steps for code generation.
Limitations
- OWL-induced invariants are not serialised into the generated TypeScript. Property characteristics (
owl:FunctionalProperty,owl:TransitiveProperty, etc.) and cross-field invariants are recorded inOwlImportResult.characteristicsandOwlImportResult.invariantsat runtime. The code generator emits the structural schema only. Register characteristics and invariants programmatically after importing the generated module if you need them at runtime. - Generated files are one-way. When the source ontology changes, regenerate the TypeScript file and commit the update. Do not hand-edit generated files; they will be overwritten on the next generation run.
Generating a full registry directory v0.12.0+
For production canonical domains, the registry-directory mode generates the same layout as the canonical bookstore example: one entities/<Name>.ts file per OWL class, plus an index.ts that imports all entities, constructs the registry, and re-exports all types and schema constants.
When to use each mode:
| Mode | When to use |
|---|---|
Single-file (--out foo.ts) | Quick demos, prototypes, CLI pipelines where one file is easier to handle |
Registry-directory (--out foo/) | Production canonical domains: mirrors the bookstore layout, each class gets its own file and type export |
The registry-directory output is structurally identical to a hand-authored domain: entity files use export const <Name>Schema = { ... } as const and export type <Name> = InferType<typeof <Name>Schema>, while index.ts constructs JsonTology.create({ baseIri, schemas }) in dependency order.
CLI
# Auto-detect: trailing slash or no .ts extension → registry-directory mode
npx json-tology owl-gen ./foaf.jsonld --out ./src/generated/foaf/
# Explicit mode flag
npx json-tology owl-gen ./foaf.jsonld --out ./src/generated/foaf --mode directoryProgrammatic API
import { generateRegistryDirectory } from 'json-tology/owl-gen';
const result = generateRegistryDirectory({
input: jsonLdStringOrObject, // string | object | QuadInterface[]
outDir: './src/generated/foaf',
name?: string, // registry constant name (e.g. 'foaf')
baseIri?: string,
sourceLabel?: string,
});
// result.entityFiles — [{ path, iri, name }, ...]
// result.indexFile — absolute path of the written index.tsRunnable examples
/**
* FOAF subset — registry-directory codegen round-trip.
*
* Demonstrates `generateRegistryDirectory` producing a canonical directory layout
* as data (no disk I/O):
* entities/<Name>.ts — one file per OWL class (as source strings)
* index.ts source — imports all entities, constructs the registry
*
* Steps:
* 1. Call `generateRegistryDirectory` and inspect the returned entity files.
* 2. Assert the expected entity paths appear in the result.
* 3. Log entity file count, path names, and indexSource length.
*
* Entity files mirror the canonical bookstore layout (`entities/<Name>.ts`):
* each file exports `<Name>Schema as const` and `type <Name> = InferType<...>`.
* The index re-exports all schemas and types, and constructs the registry.
*
* Browser-safe: no node:fs, node:path, or node:url.
*/
import { generateRegistryDirectory } from '../../../src/owl-gen/index.js';
import { foafSubset } from '../ontologies/foaf-subset.js';
// ---------------------------------------------------------------------------
// Step 1: generate the registry directory as data (no disk I/O)
// ---------------------------------------------------------------------------
const genResult = generateRegistryDirectory({
'input': foafSubset,
'name': 'foaf',
'sourceLabel': 'examples/docs/ontologies/foaf-subset.jsonld'
});
// Three classes + four property stubs = 7 entity files
console.assert(
genResult.entityFiles.length === 7,
`Expected 7 entity files, got ${genResult.entityFiles.length}`
);
console.log(`generateRegistryDirectory: ${genResult.entityFiles.length} entity files`);
// ---------------------------------------------------------------------------
// Step 2: assert expected entity paths appear in the result
// ---------------------------------------------------------------------------
const expectedPaths = [
'entities/Agent.ts',
'entities/Name.ts',
'entities/Mbox.ts',
'entities/Knows.ts',
'entities/Member.ts',
'entities/Group.ts',
'entities/Person.ts'
];
for (const expectedPath of expectedPaths) {
const found = genResult.entityFiles.some((entityFile) => {
return entityFile.path === expectedPath;
});
console.assert(found, `Expected entity file path: ${expectedPath}`);
}
console.log('Entity paths:', genResult.entityFiles.map((entityFile) => {
return entityFile.path;
}).join(', '));
// ---------------------------------------------------------------------------
// Step 3: log salient facts about the generated data
// ---------------------------------------------------------------------------
console.log('indexSource length:', genResult.indexSource.length);
console.assert(genResult.indexSource.includes('JsonTology'), 'indexSource must reference JsonTology');
console.assert(genResult.indexSource.includes('AgentSchema'), 'indexSource must reference AgentSchema');
// PersonSchema carries disjointWith — verify it is preserved in the generated source
const personFile = genResult.entityFiles.find((entityFile) => {
return entityFile.path === 'entities/Person.ts';
});
console.assert(personFile !== undefined, 'entities/Person.ts must be present');
if (personFile !== undefined) {
console.assert(
personFile.source.includes('disjointWith'),
'Person entity source must preserve disjointWith annotation'
);
console.log('entities/Person.ts source length:', personFile.source.length);
console.log('disjointWith preserved in Person.ts:', personFile.source.includes('disjointWith'));
}
Entity file ↔ canonical bookstore symmetry
Each generated entities/<Name>.ts follows the same convention as examples/docs/bookstore/entities/<Name>.ts: a single export const <Name>Schema = { ... } as const plus a co-located export type <Name> = InferType<typeof <Name>Schema>. Cross-class $refs remain as raw IRI strings inside the schema literal; the registry resolves them at construction time just as it does for hand-authored schemas.
Real-ontology round-trip examples v0.11.1+
The examples below run the full fromTbox → generateFromTbox → validate pipeline against three real-world standard vocabularies. Each fixture is a hand-authored concise subset, not the full upstream ontology, so it fits on a page and compiles in milliseconds.
The committed generated files (examples/docs/ontologies/generated/*.generated.ts) are refreshed by running npm run regen:ontology-fixtures when the codegen output format changes.
FOAF (Friend of a Friend)
FOAF is a classic semantic-web vocabulary for describing people and their social relationships. The interesting round-trip detail: owl:disjointWith between foaf:Person and foaf:Group is encoded symmetrically; both class schemas carry disjointWith pointing at each other.
Input ontology fixture:
{
"_comment": "FOAF subset for json-tology codegen round-trip demo. Not a complete FOAF projection. Covers Agent, Person, Group classes; name, mbox, knows, member properties; domain/range; rdfs:label/rdfs:comment annotations; owl:disjointWith between Person and Group.",
"@context": {
"owl": "http://www.w3.org/2002/07/owl#",
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"foaf": "http://xmlns.com/foaf/0.1/"
},
"@graph": [
{
"@id": "http://xmlns.com/foaf/0.1/Agent",
"@type": "owl:Class",
"rdfs:label": "Agent",
"rdfs:comment": "An agent (e.g. a person, group, software or physical artifact)."
},
{
"@id": "http://xmlns.com/foaf/0.1/Person",
"@type": "owl:Class",
"rdfs:subClassOf": { "@id": "http://xmlns.com/foaf/0.1/Agent" },
"owl:disjointWith": { "@id": "http://xmlns.com/foaf/0.1/Group" },
"rdfs:label": "Person",
"rdfs:comment": "A person."
},
{
"@id": "http://xmlns.com/foaf/0.1/Group",
"@type": "owl:Class",
"rdfs:subClassOf": { "@id": "http://xmlns.com/foaf/0.1/Agent" },
"owl:disjointWith": { "@id": "http://xmlns.com/foaf/0.1/Person" },
"rdfs:label": "Group",
"rdfs:comment": "A class of Agents."
},
{
"@id": "http://xmlns.com/foaf/0.1/name",
"@type": "owl:DatatypeProperty",
"rdfs:label": "name",
"rdfs:comment": "A name for some thing.",
"rdfs:domain": { "@id": "http://xmlns.com/foaf/0.1/Agent" },
"rdfs:range": { "@id": "xsd:string" }
},
{
"@id": "http://xmlns.com/foaf/0.1/mbox",
"@type": "owl:DatatypeProperty",
"rdfs:label": "personal mailbox",
"rdfs:comment": "A personal mailbox, ie. an Internet mailbox associated with exactly one owner.",
"rdfs:domain": { "@id": "http://xmlns.com/foaf/0.1/Agent" },
"rdfs:range": { "@id": "xsd:string" }
},
{
"@id": "http://xmlns.com/foaf/0.1/knows",
"@type": "owl:ObjectProperty",
"rdfs:label": "knows",
"rdfs:comment": "A person known by this person (indicating some level of reciprocated interaction between the parties).",
"rdfs:domain": { "@id": "http://xmlns.com/foaf/0.1/Person" },
"rdfs:range": { "@id": "http://xmlns.com/foaf/0.1/Person" }
},
{
"@id": "http://xmlns.com/foaf/0.1/member",
"@type": "owl:ObjectProperty",
"rdfs:label": "member",
"rdfs:comment": "Indicates a member of a Group.",
"rdfs:domain": { "@id": "http://xmlns.com/foaf/0.1/Group" },
"rdfs:range": { "@id": "http://xmlns.com/foaf/0.1/Agent" }
}
]
}Generated TypeScript (npm run regen:ontology-fixtures):
// ============================================================
// AUTO-GENERATED — DO NOT EDIT
// Generated: 2026-05-20T04:39:50.516Z
// Source: examples/docs/ontologies/foaf-subset.jsonld
// ============================================================
import { JsonTology } from 'json-tology';
import type { InferType } from 'json-tology/types';
export const AgentSchema = {
"$id": "http://xmlns.com/foaf/0.1/Agent",
"description": "An agent (e.g. a person, group, software or physical artifact).",
"properties": {
"mbox": {
"type": "string",
},
"name": {
"type": "string",
},
},
"required": [],
"title": "Agent",
"type": "object",
} as const;
export const NameSchema = {
"$id": "http://xmlns.com/foaf/0.1/name",
"description": "A name for some thing.",
"title": "name",
} as const;
export const MboxSchema = {
"$id": "http://xmlns.com/foaf/0.1/mbox",
"description": "A personal mailbox, ie. an Internet mailbox associated with exactly one owner.",
"title": "personal mailbox",
} as const;
export const KnowsSchema = {
"$id": "http://xmlns.com/foaf/0.1/knows",
"description": "A person known by this person (indicating some level of reciprocated interaction between the parties).",
"title": "knows",
} as const;
export const MemberSchema = {
"$id": "http://xmlns.com/foaf/0.1/member",
"description": "Indicates a member of a Group.",
"title": "member",
} as const;
export const GroupSchema = {
"$id": "http://xmlns.com/foaf/0.1/Group",
"allOf": [
{
"$ref": "http://xmlns.com/foaf/0.1/Agent",
},
],
"description": "A class of Agents.",
"disjointWith": "http://xmlns.com/foaf/0.1/Person",
"properties": {
"member": {
"$ref": "http://xmlns.com/foaf/0.1/Agent",
},
},
"required": [],
"title": "Group",
"type": "object",
} as const;
export const PersonSchema = {
"$id": "http://xmlns.com/foaf/0.1/Person",
"allOf": [
{
"$ref": "http://xmlns.com/foaf/0.1/Agent",
},
],
"description": "A person.",
"disjointWith": "http://xmlns.com/foaf/0.1/Group",
"properties": {
"knows": {
"$ref": "http://xmlns.com/foaf/0.1/Person",
},
},
"required": [],
"title": "Person",
"type": "object",
} as const;
export const foafSchemas = [
AgentSchema,
NameSchema,
MboxSchema,
KnowsSchema,
MemberSchema,
GroupSchema,
PersonSchema,
] as const;
export const foaf = JsonTology.create({
"baseIri": "http://xmlns.com/foaf/0.1",
"schemas": foafSchemas,
} as const);
export type Agent = InferType<typeof AgentSchema>;
export type Name = InferType<typeof NameSchema>;
export type Mbox = InferType<typeof MboxSchema>;
export type Knows = InferType<typeof KnowsSchema>;
export type Member = InferType<typeof MemberSchema>;
export type Group = InferType<typeof GroupSchema>;
export type Person = InferType<typeof PersonSchema>;
// ============================================================
// END AUTO-GENERATED
// ============================================================Runnable round-trip:
/**
* FOAF subset — real-ontology codegen round-trip.
*
* FOAF (Friend of a Friend) is a classic semantic-web vocabulary for describing
* people and their social relationships. This example demonstrates a round-trip
* against a hand-authored FOAF subset:
*
* 1. Import the foaf-subset data and call `JsonTology.fromTbox` (runtime path).
* 2. Generate TypeScript source via `generateFromTbox` and log salient facts.
* 3. Validate a Bastian-style `foaf:Agent` instance against the runtime schema.
* 4. Assert `InferType<typeof FoafAgent>` narrows name + mbox fields correctly
* at the compile-time level.
*
* Notable round-trip behaviour: `owl:disjointWith` between `foaf:Person` and
* `foaf:Group` is preserved as `disjointWith: "..."` on both class schemas.
*
* Browser-safe: no node:fs, node:path, or node:url.
*/
import type { InferType } from '../../../src/types/index.js';
import { JsonTology } from '../../../src/index.js';
import { generateFromTbox } from '../../../src/owl-gen/index.js';
import { foafSubset } from '../ontologies/foaf-subset.js';
// ---------------------------------------------------------------------------
// Step 1: runtime fromTbox — import the FOAF JSON-LD data directly
// ---------------------------------------------------------------------------
const result = JsonTology.fromTbox(foafSubset);
// Three classes: Agent, Person, Group (plus property stubs for annotations)
console.assert(result.schemas.length >= 3, `Expected at least 3 schemas, got ${result.schemas.length}`);
console.assert(result.unsupported.length === 0, `Expected 0 unsupported axioms, got ${result.unsupported.length}`);
// owl:disjointWith is symmetric — both Person and Group carry the annotation
const personSchema = result.schemas.find((schema) => {
return schema.$id === 'http://xmlns.com/foaf/0.1/Person';
});
const groupSchema = result.schemas.find((schema) => {
return schema.$id === 'http://xmlns.com/foaf/0.1/Group';
});
console.assert(
personSchema !== undefined,
'foaf:Person schema must be present after fromTbox'
);
const personRec = personSchema as Record<string, unknown>;
const groupRec = groupSchema as Record<string, unknown>;
console.assert(
personRec.disjointWith === 'http://xmlns.com/foaf/0.1/Group',
'owl:disjointWith preserved: Person disjoint with Group'
);
console.assert(
groupRec.disjointWith === 'http://xmlns.com/foaf/0.1/Person',
'owl:disjointWith preserved: Group disjoint with Person (symmetric)'
);
console.log(`fromTbox: ${result.schemas.length} schemas, ${result.unsupported.length} unsupported axioms`);
console.log('disjointWith (Person -> Group):', personRec.disjointWith);
// ---------------------------------------------------------------------------
// Step 2: codegen — generate source and log salient facts
// ---------------------------------------------------------------------------
const generatedSrc = generateFromTbox({
'input': foafSubset,
'name': 'foaf',
'sourceLabel': 'examples/docs/ontologies/foaf-subset.jsonld'
});
console.assert(generatedSrc.includes('export const AgentSchema'), 'Generated source must export AgentSchema');
console.assert(generatedSrc.includes('export const PersonSchema'), 'Generated source must export PersonSchema');
console.assert(generatedSrc.includes('export const GroupSchema'), 'Generated source must export GroupSchema');
console.assert(generatedSrc.includes('disjointWith'), 'Generated source must preserve disjointWith annotation');
console.log('Generated source length:', generatedSrc.length);
console.log('Contains AgentSchema export:', generatedSrc.includes('export const AgentSchema'));
console.log('Contains disjointWith annotation:', generatedSrc.includes('disjointWith'));
// ---------------------------------------------------------------------------
// Step 3: validate foaf:Agent instances against the runtime-imported schema
// ---------------------------------------------------------------------------
const agentSchema = result.schemas.find((schema) => {
return schema.$id === 'http://xmlns.com/foaf/0.1/Agent';
});
console.assert(agentSchema !== undefined, 'foaf:Agent schema must be present');
if (agentSchema !== undefined && typeof agentSchema.$id === 'string') {
const agentSchemaRec = agentSchema as Record<string, unknown> & { '$id': string };
const jt = JsonTology.create({
'baseIri': 'http://xmlns.com/foaf/0.1/',
'enableStrictGraph': false,
'schemas': [agentSchemaRec]
});
// Bastian Balthazar Bux as foaf:Agent — name + mbox are owl:DatatypeProperty
// on foaf:Agent, so they appear as string properties on AgentSchema.
const bastian = {
'mbox': 'bastian@fantastica.example',
'name': 'Bastian Balthazar Bux'
};
const bastianResult = jt.validate(agentSchemaRec, bastian);
console.assert(bastianResult.ok, `Bastian Balthazar Bux must validate as foaf:Agent; errors: ${JSON.stringify(bastianResult)}`);
console.log('Bastian Balthazar Bux validates as foaf:Agent:', bastianResult.ok);
const cornelia = {
'mbox': 'cornelia@funke.example',
'name': 'Cornelia Funke'
};
const corneliaResult = jt.validate(agentSchemaRec, cornelia);
console.assert(corneliaResult.ok, `Cornelia Funke must validate as foaf:Agent; errors: ${JSON.stringify(corneliaResult)}`);
console.log('Cornelia Funke validates as foaf:Agent:', corneliaResult.ok);
}
console.log('PersonSchema.$id:', personRec.$id);
console.assert(
personRec.disjointWith === 'http://xmlns.com/foaf/0.1/Group',
'PersonSchema.disjointWith preserved in runtime schemas'
);
console.log('PersonSchema.disjointWith preserved:', personRec.disjointWith);
// ---------------------------------------------------------------------------
// Step 4: compile-time type narrowing with InferType
// ---------------------------------------------------------------------------
type FoafAgent = InferType<{
readonly '$id': 'http://xmlns.com/foaf/0.1/Agent';
readonly 'properties': {
readonly 'mbox': { readonly 'type': 'string' };
readonly 'name': { readonly 'type': 'string' };
};
readonly 'required': [];
readonly 'type': 'object';
}>;
const bastianTyped: FoafAgent = { 'name': 'Bastian Balthazar Bux' };
console.assert(
typeof bastianTyped.name === 'string',
'InferType narrows foaf:Agent.name to string'
);
console.log('InferType<FoafAgent>.name narrows to string:', typeof bastianTyped.name === 'string');
DCAT-AP (Data Catalog Vocabulary)
DCAT is a W3C recommendation for describing data catalogs and datasets published on the Web. The interesting round-trip detail: the rdfs:subClassOf chain reaches dcterms:Resource, an external IRI not defined in this subset. fromTbox handles this gracefully: dcterms:Resource becomes a class stub, and dcat:Dataset and dcat:Catalog carry allOf: [{ $ref: "http://purl.org/dc/terms/Resource" }] pointing to it.
Input ontology fixture:
{
"_comment": "DCAT-AP subset for json-tology codegen round-trip demo. Not a complete DCAT-AP projection. Covers Dataset, Distribution, Catalog classes; title, description, distribution, accessURL properties; rdfs:subClassOf chain reaching dcterms:Resource as an external IRI.",
"@context": {
"owl": "http://www.w3.org/2002/07/owl#",
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"dcat": "http://www.w3.org/ns/dcat#",
"dcterms": "http://purl.org/dc/terms/"
},
"@graph": [
{
"@id": "http://purl.org/dc/terms/Resource",
"@type": "owl:Class",
"rdfs:label": "Resource",
"rdfs:comment": "Anything described by RDF (external Dublin Core class, kept as a stub)."
},
{
"@id": "http://www.w3.org/ns/dcat#Dataset",
"@type": "owl:Class",
"rdfs:subClassOf": { "@id": "http://purl.org/dc/terms/Resource" },
"rdfs:label": "Dataset",
"rdfs:comment": "A collection of data, published or curated by a single agent."
},
{
"@id": "http://www.w3.org/ns/dcat#Distribution",
"@type": "owl:Class",
"rdfs:label": "Distribution",
"rdfs:comment": "A specific representation of a dataset."
},
{
"@id": "http://www.w3.org/ns/dcat#Catalog",
"@type": "owl:Class",
"rdfs:subClassOf": { "@id": "http://purl.org/dc/terms/Resource" },
"rdfs:label": "Catalog",
"rdfs:comment": "A curated collection of metadata about resources."
},
{
"@id": "http://www.w3.org/ns/dcat#title",
"@type": "owl:DatatypeProperty",
"rdfs:label": "title",
"rdfs:comment": "A name given to the resource.",
"rdfs:domain": { "@id": "http://purl.org/dc/terms/Resource" },
"rdfs:range": { "@id": "xsd:string" }
},
{
"@id": "http://www.w3.org/ns/dcat#description",
"@type": "owl:DatatypeProperty",
"rdfs:label": "description",
"rdfs:comment": "A free-text account of the resource.",
"rdfs:domain": { "@id": "http://purl.org/dc/terms/Resource" },
"rdfs:range": { "@id": "xsd:string" }
},
{
"@id": "http://www.w3.org/ns/dcat#distribution",
"@type": "owl:ObjectProperty",
"rdfs:label": "distribution",
"rdfs:comment": "An available distribution of the dataset.",
"rdfs:domain": { "@id": "http://www.w3.org/ns/dcat#Dataset" },
"rdfs:range": { "@id": "http://www.w3.org/ns/dcat#Distribution" }
},
{
"@id": "http://www.w3.org/ns/dcat#accessURL",
"@type": "owl:DatatypeProperty",
"rdfs:label": "access URL",
"rdfs:comment": "A URL of the resource that gives access to a distribution. Range is xsd:string in this subset (xsd:anyURI would produce a { format: uri } inline constraint that requires enableStrictGraph: false).",
"rdfs:domain": { "@id": "http://www.w3.org/ns/dcat#Distribution" },
"rdfs:range": { "@id": "xsd:string" }
}
]
}Generated TypeScript:
// ============================================================
// AUTO-GENERATED — DO NOT EDIT
// Generated: 2026-05-20T04:39:50.520Z
// Source: examples/docs/ontologies/dcat-subset.jsonld
//
// WARNING: IRI name collisions detected. Suffixed names used:
// Distribution (_2, _3, ...)
// ============================================================
import { JsonTology } from 'json-tology';
import type { InferType } from 'json-tology/types';
export const ResourceSchema = {
"$id": "http://purl.org/dc/terms/Resource",
"description": "Anything described by RDF (external Dublin Core class, kept as a stub).",
"properties": {
"description": {
"type": "string",
},
"title": {
"type": "string",
},
},
"required": [],
"title": "Resource",
"type": "object",
} as const;
export const DistributionSchema = {
"$id": "http://www.w3.org/ns/dcat#Distribution",
"description": "A specific representation of a dataset.",
"properties": {
"accessURL": {
"type": "string",
},
},
"required": [],
"title": "Distribution",
"type": "object",
} as const;
export const TitleSchema = {
"$id": "http://www.w3.org/ns/dcat#title",
"description": "A name given to the resource.",
"title": "title",
} as const;
export const DescriptionSchema = {
"$id": "http://www.w3.org/ns/dcat#description",
"description": "A free-text account of the resource.",
"title": "description",
} as const;
export const Distribution_2Schema = {
"$id": "http://www.w3.org/ns/dcat#distribution",
"description": "An available distribution of the dataset.",
"title": "distribution",
} as const;
export const AccessURLSchema = {
"$id": "http://www.w3.org/ns/dcat#accessURL",
"description": "A URL of the resource that gives access to a distribution. Range is xsd:string in this subset (xsd:anyURI would produce a { format: uri } inline constraint that requires enableStrictGraph: false).",
"title": "access URL",
} as const;
export const CatalogSchema = {
"$id": "http://www.w3.org/ns/dcat#Catalog",
"allOf": [
{
"$ref": "http://purl.org/dc/terms/Resource",
},
],
"description": "A curated collection of metadata about resources.",
"properties": {},
"required": [],
"title": "Catalog",
"type": "object",
} as const;
export const DatasetSchema = {
"$id": "http://www.w3.org/ns/dcat#Dataset",
"allOf": [
{
"$ref": "http://purl.org/dc/terms/Resource",
},
],
"description": "A collection of data, published or curated by a single agent.",
"properties": {
"distribution": {
"$ref": "http://www.w3.org/ns/dcat#Distribution",
},
},
"required": [],
"title": "Dataset",
"type": "object",
} as const;
export const dcatSchemas = [
ResourceSchema,
DistributionSchema,
TitleSchema,
DescriptionSchema,
Distribution_2Schema,
AccessURLSchema,
CatalogSchema,
DatasetSchema,
] as const;
export const dcat = JsonTology.create({
"baseIri": "http://purl.org/dc/terms",
"schemas": dcatSchemas,
} as const);
export type Resource = InferType<typeof ResourceSchema>;
export type Distribution = InferType<typeof DistributionSchema>;
export type Title = InferType<typeof TitleSchema>;
export type Description = InferType<typeof DescriptionSchema>;
export type Distribution_2 = InferType<typeof Distribution_2Schema>;
export type AccessURL = InferType<typeof AccessURLSchema>;
export type Catalog = InferType<typeof CatalogSchema>;
export type Dataset = InferType<typeof DatasetSchema>;
// ============================================================
// END AUTO-GENERATED
// ============================================================Runnable round-trip:
/**
* DCAT-AP subset — real-ontology codegen round-trip.
*
* DCAT (Data Catalog Vocabulary) is a W3C recommendation for describing data
* catalogs and datasets published on the Web. This example demonstrates a round-trip
* against a hand-authored DCAT-AP subset:
*
* 1. Import the dcat-subset data and call `JsonTology.fromTbox` (runtime path).
* 2. Generate TypeScript source via `generateFromTbox` and log salient facts.
* 3. Validate a Neverending Story-flavoured dataset instance.
* 4. Assert `InferType<typeof DcatResource>` narrows title + description fields
* at the compile-time level.
*
* Notable round-trip behaviour: the `rdfs:subClassOf` chain reaches `dcterms:Resource`,
* an external IRI not defined in this subset. `fromTbox` handles this gracefully —
* `dcterms:Resource` becomes a class stub, and `dcat:Dataset` and `dcat:Catalog`
* carry `allOf: [{ $ref: "http://purl.org/dc/terms/Resource" }]` pointing to it.
*
* Browser-safe: no node:fs, node:path, or node:url.
*/
import type { InferType } from '../../../src/types/index.js';
import { JsonTology } from '../../../src/index.js';
import { generateFromTbox } from '../../../src/owl-gen/index.js';
import { dcatSubset } from '../ontologies/dcat-subset.js';
// ---------------------------------------------------------------------------
// Step 1: runtime fromTbox — import the DCAT-AP JSON-LD data directly
// ---------------------------------------------------------------------------
const result = JsonTology.fromTbox(dcatSubset);
// Four classes: Resource (external), Dataset, Distribution, Catalog (plus property stubs)
console.assert(result.schemas.length >= 4, `Expected at least 4 schemas, got ${result.schemas.length}`);
console.assert(result.unsupported.length === 0, `Expected 0 unsupported axioms, got ${result.unsupported.length}`);
// rdfs:subClassOf chain: Dataset -> dcterms:Resource (external IRI stub)
const datasetSchema = result.schemas.find((schema) => {
return schema.$id === 'http://www.w3.org/ns/dcat#Dataset';
});
const resourceSchema = result.schemas.find((schema) => {
return schema.$id === 'http://purl.org/dc/terms/Resource';
});
console.assert(datasetSchema !== undefined, 'dcat:Dataset schema must be present after fromTbox');
console.assert(resourceSchema !== undefined, 'dcterms:Resource (external) stub must be present after fromTbox');
const datasetRec = datasetSchema as Record<string, unknown>;
const datasetAllOf = datasetRec.allOf as Array<Record<string, unknown>> | undefined;
const inheritsFromResource = Array.isArray(datasetAllOf)
&& datasetAllOf.some((entry) => {
return entry.$ref === 'http://purl.org/dc/terms/Resource';
});
console.assert(
inheritsFromResource,
'subClassOf preserved: dcat:Dataset -> dcterms:Resource (external IRI)'
);
console.log(`fromTbox: ${result.schemas.length} schemas, ${result.unsupported.length} unsupported axioms`);
console.log('subClassOf (Dataset -> dcterms:Resource):', inheritsFromResource);
console.log('dcterms:Resource stub present (external IRI):', resourceSchema !== undefined);
// ---------------------------------------------------------------------------
// Step 2: codegen — generate source and log salient facts
// ---------------------------------------------------------------------------
const generatedSrc = generateFromTbox({
'input': dcatSubset,
'name': 'dcat',
'sourceLabel': 'examples/docs/ontologies/dcat-subset.jsonld'
});
console.assert(generatedSrc.includes('export const DatasetSchema'), 'Generated source must export DatasetSchema');
console.assert(generatedSrc.includes('export const DistributionSchema'), 'Generated source must export DistributionSchema');
console.assert(generatedSrc.includes('allOf'), 'Generated source must preserve subClassOf as allOf');
console.log('Generated source length:', generatedSrc.length);
console.log('Contains DatasetSchema export:', generatedSrc.includes('export const DatasetSchema'));
console.log('Contains allOf (subClassOf):', generatedSrc.includes('allOf'));
// ---------------------------------------------------------------------------
// Step 3: validate DCAT instances against the runtime-imported schemas
//
// Note on $ref resolution with dcat# IRIs: the DCAT namespace uses '#' as a
// namespace separator (http://www.w3.org/ns/dcat#Dataset), which json-tology's
// ref walker interprets as a JSON pointer fragment separator. Cross-schema refs
// between dcat# and dcterms/ namespaces therefore cannot be compiled in the
// single-registry path. We validate each class schema in isolation against a
// per-class registry, which accurately reflects how a consumer uses the vocabulary.
// ---------------------------------------------------------------------------
const distributionSchema = result.schemas.find((schema) => {
return schema.$id === 'http://www.w3.org/ns/dcat#Distribution';
});
if (distributionSchema !== undefined && typeof distributionSchema.$id === 'string') {
const distributionRec = distributionSchema as Record<string, unknown> & { '$id': string };
const distJt = JsonTology.create({
'baseIri': 'http://www.w3.org/ns/dcat#',
'enableStrictGraph': false,
'schemas': [distributionRec]
});
const neverendingDistribution = { 'accessURL': 'https://fantastica.example/data/realms.csv' };
const distResult = distJt.validate(distributionRec, neverendingDistribution);
console.assert(distResult.ok, `Distribution must validate; errors: ${JSON.stringify(distResult)}`);
console.log('Distribution validates (accessURL as string):', distResult.ok);
}
if (resourceSchema !== undefined && typeof resourceSchema.$id === 'string') {
const resourceRec = resourceSchema as Record<string, unknown> & { '$id': string };
const resourceJt = JsonTology.create({
'baseIri': 'http://purl.org/dc/terms',
'enableStrictGraph': false,
'schemas': [resourceRec]
});
const neverendingResource = {
'description': 'Open dataset of fictional realms and their inhabitants',
'title': 'Fantastica Open Realm Registry'
};
const resourceResult = resourceJt.validate(resourceRec, neverendingResource);
console.assert(resourceResult.ok, `Resource must validate; errors: ${JSON.stringify(resourceResult)}`);
console.log('Neverending Story resource validates as dcterms:Resource:', resourceResult.ok);
}
// dcat:Dataset allOf chain is preserved in the runtime schemas
console.assert(inheritsFromResource, 'subClassOf preserved in DatasetSchema');
console.log('DatasetSchema allOf -> dcterms:Resource preserved:', inheritsFromResource);
// ---------------------------------------------------------------------------
// Step 4: compile-time type narrowing with InferType
// ---------------------------------------------------------------------------
type DcatResource = InferType<{
readonly '$id': 'http://purl.org/dc/terms/Resource';
readonly 'properties': {
readonly 'description': { readonly 'type': 'string' };
readonly 'title': { readonly 'type': 'string' };
};
readonly 'required': [];
readonly 'type': 'object';
}>;
const fantasticaResource: DcatResource = { 'title': 'Fantastica Open Realm Registry' };
console.assert(
typeof fantasticaResource.title === 'string',
'InferType narrows dcat:Resource.title to string'
);
console.log('InferType<DcatResource>.title narrows to string:', typeof fantasticaResource.title === 'string');
schema.org (Structured Data Vocabulary)
schema.org is a collaborative vocabulary for structured data on the Web, widely used for search-engine markup and data exchange. The interesting round-trip detail: schema:IsbnType is declared as an rdfs:Datatype with an owl:withRestrictions XSD pattern facet (^\d{13}$). This round-trips losslessly: the generated IsbnTypeSchema carries type: 'string', pattern: '^\d{13}$' and BookSchema.properties.isbn is a $ref pointing to IsbnTypeSchema.
Input ontology fixture:
{
"_comment": "schema.org subset for json-tology codegen round-trip demo. Not a complete schema.org projection. Covers Book, Person, Organization classes; author, publisher, name, isbn properties; rdfs:Datatype XSD-facet restriction on isbn (pattern ^\\d{13}$) that round-trips losslessly through fromTbox.",
"@context": {
"owl": "http://www.w3.org/2002/07/owl#",
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"schema": "https://schema.org/"
},
"@graph": [
{
"@id": "https://schema.org/Thing",
"@type": "owl:Class",
"rdfs:label": "Thing",
"rdfs:comment": "The most generic type."
},
{
"@id": "https://schema.org/Person",
"@type": "owl:Class",
"rdfs:subClassOf": { "@id": "https://schema.org/Thing" },
"rdfs:label": "Person",
"rdfs:comment": "A person (alive, dead, undead, or fictional)."
},
{
"@id": "https://schema.org/Organization",
"@type": "owl:Class",
"rdfs:subClassOf": { "@id": "https://schema.org/Thing" },
"rdfs:label": "Organization",
"rdfs:comment": "An organization such as a school, NGO, corporation, club, etc."
},
{
"@id": "https://schema.org/Book",
"@type": "owl:Class",
"rdfs:subClassOf": { "@id": "https://schema.org/Thing" },
"rdfs:label": "Book",
"rdfs:comment": "A book."
},
{
"@id": "https://schema.org/IsbnType",
"@type": "rdfs:Datatype",
"rdfs:label": "IsbnType",
"rdfs:comment": "A 13-digit ISBN string. XSD-facet restriction: pattern ^\\d{13}$.",
"owl:onDatatype": { "@id": "xsd:string" },
"owl:withRestrictions": {
"@list": [{ "xsd:pattern": "^\\d{13}$" }]
}
},
{
"@id": "https://schema.org/name",
"@type": "owl:DatatypeProperty",
"rdfs:label": "name",
"rdfs:comment": "The name of the item.",
"rdfs:domain": { "@id": "https://schema.org/Thing" },
"rdfs:range": { "@id": "xsd:string" }
},
{
"@id": "https://schema.org/Book#isbn",
"@type": "owl:ObjectProperty",
"rdfs:label": "isbn",
"rdfs:comment": "The ISBN of the book.",
"rdfs:domain": { "@id": "https://schema.org/Book" },
"rdfs:range": { "@id": "https://schema.org/IsbnType" }
},
{
"@id": "https://schema.org/author",
"@type": "owl:ObjectProperty",
"rdfs:label": "author",
"rdfs:comment": "The author of this content.",
"rdfs:domain": { "@id": "https://schema.org/Book" },
"rdfs:range": { "@id": "https://schema.org/Person" }
},
{
"@id": "https://schema.org/publisher",
"@type": "owl:ObjectProperty",
"rdfs:label": "publisher",
"rdfs:comment": "The publisher of the creative work.",
"rdfs:domain": { "@id": "https://schema.org/Book" },
"rdfs:range": { "@id": "https://schema.org/Organization" }
}
]
}Generated TypeScript:
// ============================================================
// AUTO-GENERATED — DO NOT EDIT
// Generated: 2026-05-20T04:39:50.522Z
// Source: examples/docs/ontologies/schema-org-subset.jsonld
// ============================================================
import { JsonTology } from 'json-tology';
import type { InferType } from 'json-tology/types';
export const ThingSchema = {
"$id": "https://schema.org/Thing",
"description": "The most generic type.",
"properties": {
"name": {
"type": "string",
},
},
"required": [],
"title": "Thing",
"type": "object",
} as const;
export const IsbnTypeSchema = {
"$id": "https://schema.org/IsbnType",
"description": "A 13-digit ISBN string. XSD-facet restriction: pattern ^\\d{13}$.",
"pattern": "^\\d{13}$",
"title": "IsbnType",
"type": "string",
} as const;
export const NameSchema = {
"$id": "https://schema.org/name",
"description": "The name of the item.",
"title": "name",
} as const;
export const IsbnSchema = {
"$id": "https://schema.org/Book#isbn",
"description": "The ISBN of the book.",
"title": "isbn",
} as const;
export const AuthorSchema = {
"$id": "https://schema.org/author",
"description": "The author of this content.",
"title": "author",
} as const;
export const PublisherSchema = {
"$id": "https://schema.org/publisher",
"description": "The publisher of the creative work.",
"title": "publisher",
} as const;
export const PersonSchema = {
"$id": "https://schema.org/Person",
"allOf": [
{
"$ref": "https://schema.org/Thing",
},
],
"description": "A person (alive, dead, undead, or fictional).",
"properties": {},
"required": [],
"title": "Person",
"type": "object",
} as const;
export const OrganizationSchema = {
"$id": "https://schema.org/Organization",
"allOf": [
{
"$ref": "https://schema.org/Thing",
},
],
"description": "An organization such as a school, NGO, corporation, club, etc.",
"properties": {},
"required": [],
"title": "Organization",
"type": "object",
} as const;
export const BookSchema = {
"$id": "https://schema.org/Book",
"allOf": [
{
"$ref": "https://schema.org/Thing",
},
],
"description": "A book.",
"properties": {
"author": {
"$ref": "https://schema.org/Person",
},
"isbn": {
"$ref": "https://schema.org/IsbnType",
},
"publisher": {
"$ref": "https://schema.org/Organization",
},
},
"required": [],
"title": "Book",
"type": "object",
} as const;
export const schemaOrgSchemas = [
ThingSchema,
IsbnTypeSchema,
NameSchema,
IsbnSchema,
AuthorSchema,
PublisherSchema,
PersonSchema,
OrganizationSchema,
BookSchema,
] as const;
export const schemaOrg = JsonTology.create({
"baseIri": "https://schema.org",
"schemas": schemaOrgSchemas,
} as const);
export type Thing = InferType<typeof ThingSchema>;
export type IsbnType = InferType<typeof IsbnTypeSchema>;
export type Name = InferType<typeof NameSchema>;
export type Isbn = InferType<typeof IsbnSchema>;
export type Author = InferType<typeof AuthorSchema>;
export type Publisher = InferType<typeof PublisherSchema>;
export type Person = InferType<typeof PersonSchema>;
export type Organization = InferType<typeof OrganizationSchema>;
export type Book = InferType<typeof BookSchema>;
// ============================================================
// END AUTO-GENERATED
// ============================================================Runnable round-trip:
/**
* schema.org subset — real-ontology codegen round-trip.
*
* schema.org is a collaborative vocabulary for structured data on the Web,
* used by search engines and data publishers. This example demonstrates a
* round-trip against a hand-authored schema.org subset:
*
* 1. Import the schema-org-subset data and call `JsonTology.fromTbox`
* (runtime path).
* 2. Generate TypeScript source via `generateFromTbox` and log salient facts.
* 3. Validate a Bastian Balthazar Bux-flavoured schema:Book instance.
* 4. Assert `InferType<typeof IsbnTypeSchema>` narrows to `string` at compile time.
*
* Notable round-trip behaviour: `schema:IsbnType` is declared as an `rdfs:Datatype`
* with an `owl:withRestrictions` XSD pattern facet (`^\d{13}$`). This round-trips
* losslessly — the generated `IsbnTypeSchema` carries `type: 'string', pattern: ...`
* and `BookSchema.properties.isbn` is a `$ref` pointing to `IsbnTypeSchema`.
*
* Browser-safe: no node:fs, node:path, or node:url.
*/
import type {
InferType, SchemaReferencesMapType
} from '../../../src/types/index.js';
import { JsonTology } from '../../../src/index.js';
import { generateFromTbox } from '../../../src/owl-gen/index.js';
import { schemaOrgSubset } from '../ontologies/schema-org-subset.js';
// ---------------------------------------------------------------------------
// Step 1: runtime fromTbox — import the schema.org JSON-LD data directly
// ---------------------------------------------------------------------------
const result = JsonTology.fromTbox(schemaOrgSubset);
// Classes: Thing, Person, Organization, Book, IsbnType (datatype), plus property stubs
console.assert(result.schemas.length >= 5, `Expected at least 5 schemas, got ${result.schemas.length}`);
console.assert(result.unsupported.length === 0, `Expected 0 unsupported axioms, got ${result.unsupported.length}`);
// XSD-facet-bearing datatype: IsbnType carries pattern ^\\d{13}$
const isbnTypeSchema = result.schemas.find((schema) => {
return schema.$id === 'https://schema.org/IsbnType';
});
console.assert(isbnTypeSchema !== undefined, 'schema:IsbnType datatype schema must be present');
const isbnTypeRec = isbnTypeSchema as Record<string, unknown>;
console.assert(
isbnTypeRec.pattern === '^\\d{13}$',
'XSD pattern facet ^\\d{13}$ round-trips losslessly on IsbnType'
);
console.assert(
isbnTypeRec.type === 'string',
'IsbnType resolves to type: string'
);
// Book -> Thing via rdfs:subClassOf
const bookSchema = result.schemas.find((schema) => {
return schema.$id === 'https://schema.org/Book';
});
const bookRec = bookSchema as Record<string, unknown>;
const bookAllOf = bookRec.allOf as Array<Record<string, unknown>> | undefined;
const bookInheritsThing = Array.isArray(bookAllOf) && bookAllOf.some((entry) => {
return entry.$ref === 'https://schema.org/Thing';
});
console.assert(bookInheritsThing, 'subClassOf preserved: schema:Book -> schema:Thing');
console.log(`fromTbox: ${result.schemas.length} schemas, ${result.unsupported.length} unsupported axioms`);
console.log('IsbnType pattern round-trips:', isbnTypeRec.pattern);
console.log('subClassOf (Book -> Thing):', bookInheritsThing);
// ---------------------------------------------------------------------------
// Step 2: codegen — generate source and log salient facts
// ---------------------------------------------------------------------------
const generatedSrc = generateFromTbox({
'input': schemaOrgSubset,
'name': 'schemaOrg',
'sourceLabel': 'examples/docs/ontologies/schema-org-subset.jsonld'
});
console.assert(generatedSrc.includes('export const BookSchema'), 'Generated source must export BookSchema');
console.assert(generatedSrc.includes('export const IsbnTypeSchema'), 'Generated source must export IsbnTypeSchema');
console.assert(generatedSrc.includes('^\\\\d{13}$'), 'Generated source must preserve ISBN pattern');
console.log('Generated source length:', generatedSrc.length);
console.log('Contains BookSchema export:', generatedSrc.includes('export const BookSchema'));
console.log('Contains IsbnTypeSchema export:', generatedSrc.includes('export const IsbnTypeSchema'));
// ---------------------------------------------------------------------------
// Step 3: validate schema.org instances against the runtime-imported schemas
// ---------------------------------------------------------------------------
const personSchema = result.schemas.find((schema) => {
return schema.$id === 'https://schema.org/Person';
});
const organizationSchema = result.schemas.find((schema) => {
return schema.$id === 'https://schema.org/Organization';
});
if (personSchema !== undefined && typeof personSchema.$id === 'string') {
const personRec = personSchema as Record<string, unknown> & { '$id': string };
const thingSchema = result.schemas.find((schema) => {
return schema.$id === 'https://schema.org/Thing';
});
const thingRec = thingSchema as (Record<string, unknown> & { '$id': string }) | undefined;
const personSchemas: Array<Record<string, unknown> & { '$id': string }> = thingRec === undefined
? [personRec]
: [
personRec,
thingRec
];
const personJt = JsonTology.create({
'baseIri': 'https://schema.org',
'enableStrictGraph': false,
'schemas': personSchemas
});
const cornelia = { 'name': 'Cornelia Funke' };
const corneliaResult = personJt.validate(personRec, cornelia);
console.assert(corneliaResult.ok, `Cornelia Funke must validate as schema:Person; errors: ${JSON.stringify(corneliaResult)}`);
console.log('Cornelia Funke validates as schema:Person:', corneliaResult.ok);
}
if (organizationSchema !== undefined && typeof organizationSchema.$id === 'string') {
const orgRec = organizationSchema as Record<string, unknown> & { '$id': string };
const thingSchema = result.schemas.find((schema) => {
return schema.$id === 'https://schema.org/Thing';
});
const thingRec = thingSchema as (Record<string, unknown> & { '$id': string }) | undefined;
const orgSchemas: Array<Record<string, unknown> & { '$id': string }> = thingRec === undefined
? [orgRec]
: [
orgRec,
thingRec
];
const orgJt = JsonTology.create({
'baseIri': 'https://schema.org',
'enableStrictGraph': false,
'schemas': orgSchemas
});
const thienemann = { 'name': 'Thienemann Verlag' };
const orgResult = orgJt.validate(orgRec, thienemann);
console.assert(orgResult.ok, `Publisher must validate as schema:Organization; errors: ${JSON.stringify(orgResult)}`);
console.log('Thienemann Verlag validates as schema:Organization:', orgResult.ok);
}
// Validate the IsbnType pattern constraint directly using the runtime schema
if (isbnTypeSchema !== undefined && typeof isbnTypeSchema.$id === 'string') {
const isbnRec = isbnTypeSchema as Record<string, unknown> & { '$id': string };
const isbnJt = JsonTology.create({
'baseIri': 'https://schema.org',
'enableStrictGraph': false,
'schemas': [isbnRec]
});
const validIsbnResult = isbnJt.validate(isbnRec, '9783551551672');
console.assert(validIsbnResult.ok, `Valid ISBN must pass pattern validation; errors: ${JSON.stringify(validIsbnResult)}`);
console.log('Valid ISBN-13 passes IsbnType pattern constraint:', validIsbnResult.ok);
const invalidIsbnResult = isbnJt.validate(isbnRec, 'not-a-isbn');
console.assert(!invalidIsbnResult.ok, 'Invalid ISBN must fail pattern validation');
console.log('Invalid ISBN string correctly rejected by IsbnType:', !invalidIsbnResult.ok);
}
// ---------------------------------------------------------------------------
// Step 4: compile-time type narrowing with InferType
// ---------------------------------------------------------------------------
const IsbnTypeSchema = {
'$id': 'https://schema.org/IsbnType',
'pattern': '^\\d{13}$',
'type': 'string'
} as const;
type IsbnType = InferType<typeof IsbnTypeSchema>;
// IsbnType carries a PatternBrandType brand; a plain string literal cannot
// satisfy the brand on its own — a branded value is produced by instantiate(),
// which validates the wire string and returns the narrowed branded type.
const isbnRegistry = JsonTology.create({
'baseIri': 'https://schema.org/',
'schemas': [IsbnTypeSchema]
});
const isbnValue: IsbnType = isbnRegistry.instantiate(IsbnTypeSchema.$id, '9783551551672');
console.assert(
typeof isbnValue === 'string',
'InferType<IsbnTypeSchema> narrows to string'
);
console.log('InferType<IsbnTypeSchema> narrows to string:', typeof isbnValue === 'string');
// The `allOf` member references schema:Thing; thread a references map carrying
// the Thing schema so the cross-schema `$ref` resolves to its inferred shape
// instead of `RefNotFound`.
type SchemaOrgRefs = SchemaReferencesMapType<readonly [{
readonly '$id': 'https://schema.org/Thing';
readonly 'properties': Record<string, never>;
readonly 'required': [];
readonly 'type': 'object';
}]>;
type SchemaPerson = InferType<{
readonly '$id': 'https://schema.org/Person';
readonly 'allOf': [{ readonly '$ref': 'https://schema.org/Thing' }];
readonly 'properties': Record<string, never>;
readonly 'required': [];
readonly 'type': 'object';
}, SchemaOrgRefs>;
const bastian: SchemaPerson = {};
console.assert(
typeof bastian === 'object',
'InferType<PersonSchema> narrows to object'
);
console.log('Bastian Balthazar Bux typed as SchemaPerson:', typeof bastian === 'object');
Related
jt.toTbox(): OWL TBox emission (the inverse operation)jt.toShacl(): SHACL shapes emission- RDF round-trip (toQuads / fromQuads): ABox data round-trip
OwlImporter(src/modules/ontology/OwlImporter.ts): low-level class if you need to reuse a single importer across multiple inputs