Compose.subClassOf / Compose.disjointWith / Compose.complementOf
Validation modes: Validation modes reference
Declaration. These three methods complete the OWL class-axiom set on Compose. They are authored as plain JSON Schema documents - every concept lives behind a method, not behind a custom keyword on the schema literal.
Compose.subClassOf(parent | parents, body): { $id, allOf: [{ $ref }, ...] }
Compose.disjointWith(other, body): { $id, disjointWith, ...body }
Compose.complementOf(other, body): { $id, not: { $ref }, ...body }subClassOf Compile-time - self-subclass is a compile-time error (SelfSubClassType brand). The body's $id cannot match any parent's $id.
disjointWith Compile-time + Runtime - compile-time brand (~jt:disjointWith) names the disjoint target; runtime enforces the constraint at validate / instantiate.
complementOf Compile-time - adds ~jt:complementOf brand naming the complement target. Runtime JSON Schema not semantics apply.
body always carries the new schema's $id and any structural keywords you would normally write inline (type, properties, required, description, etc.).
Use this when
subClassOf- you want explicit taxonomic narrowing with one OR multiple parents. Emitsrdfs:subClassOfper parent in the OWL TBox.disjointWith- two classes share no instances (e.g.WeaponandArmor). Emitsowl:disjointWith.complementOf- a class is the negation of another (e.g.NonHumanRaceis everything that is notHumanRace). Emitsowl:complementOf.
Don't use this when
- You only need property-merging with a single parent - use
Compose.extend, which is structurally equivalent (both produceallOf + $ref) but signals "extension" rather than "is-a". - You want type aliasing without OWL semantics - use
Compose.equivalent. - You want individual-level identity (
owl:sameAsbetween two ABox individuals) - useJsonTology.prototype.sameAs. Class axioms operate on the TBox layer.
Runnable examples
Example 1: single-parent subclass
/**
* Compose.subClassOf — Example: GenreBook as a subclass of Book
* Demonstrates: single-parent subClassOf, inherited property materialization,
* and rdfs:subClassOf emission in the OWL TBox.
*
* GenreBookSchema is a subclass of Book with an added `genre` property.
* Registering via JsonTology.create and calling instantiate() exercises the
* inherited fields. The TBox JSON-LD carries an rdfs:subClassOf link from
* GenreBook to Book.
*/
import {
Compose, JsonTology
} from '../../../src/index.js';
import { RDFS } from '../../../src/constants/IRI.js';
import { BookSchema } from '../bookstore/index.js';
// GenreBook is a Book subclass with one additional own property.
// body.$id must not already appear in the parent schema — no collision here.
const GenreBookSchema = Compose.subClassOf(BookSchema, {
'$id': 'urn:bookstore:GenreBook',
'properties': { 'genre': { '$ref': 'urn:bookstore:Title' } },
'required': ['genre'],
'type': 'object'
} as const);
// Register both schemas — GenreBook $refs Book so Book must come first.
const jt = JsonTology.create({
'baseIri': 'https://bookstore.example',
'schemas': [
BookSchema,
GenreBookSchema
] as const
});
// TBox carries rdfs:subClassOf linking GenreBook → Book.
const tbox = JSON.parse(jt.toTbox().jsonLd()) as {
'@graph'?: ReadonlyArray<Record<string, unknown>>;
};
const graph = tbox['@graph'] ?? [];
const genreNode = graph.find((node) => {
return node['@id'] === GenreBookSchema.$id;
});
console.assert(genreNode !== undefined, 'GenreBook appears in TBox');
const subClassOf = genreNode?.[RDFS.subClassOf] as ReadonlyArray<{ '@id': string }> | undefined;
console.assert(
Array.isArray(subClassOf),
'GenreBook node carries rdfs:subClassOf as array'
);
const refersToBook = (subClassOf ?? []).some((entry) => {
return entry['@id'] === BookSchema.$id;
});
console.assert(refersToBook, 'rdfs:subClassOf includes Book IRI');
console.log(
'GenreBook rdfs:subClassOf Book:',
refersToBook,
'| subClassOf entries:',
(subClassOf ?? []).map((entry) => {
return entry['@id'];
})
);
Example 2: disjoint classes
/**
* Compose.disjointWith — Example: EBook and PrintBook share no instances
* Demonstrates: disjointWith axiom emits owl:disjointWith in the OWL TBox.
*
* DigitalEditionSchema and PhysicalEditionSchema are subclasses of Book that
* are declared disjoint — no individual book copy can be both a digital
* download and a physical artefact simultaneously.
*
* The TBox JSON-LD carries:
* urn:bookstore:PhysicalEdition owl:disjointWith urn:bookstore:DigitalEdition
*/
import {
Compose, JsonTology
} from '../../../src/index.js';
import { OWL } from '../../../src/constants/IRI.js';
import { BookSchema } from '../bookstore/index.js';
// DigitalEdition — a Book subclass for downloadable formats.
const DigitalEditionBase = Compose.subClassOf(BookSchema, {
'$id': 'urn:bookstore:DigitalEdition',
'properties': {
'fileFormat': {
'enum': [
'epub',
'pdf'
],
'type': 'string'
}
},
'required': ['fileFormat'],
'type': 'object'
} as const);
// PhysicalEdition — a Book subclass for printed formats, declared disjoint with DigitalEdition.
const PhysicalEditionBase = Compose.subClassOf(BookSchema, {
'$id': 'urn:bookstore:PhysicalEdition',
'properties': {
'binding': {
'enum': [
'hardcover',
'paperback'
],
'type': 'string'
}
},
'required': ['binding'],
'type': 'object'
} as const);
const PhysicalEditionSchema = Compose.disjointWith(DigitalEditionBase, PhysicalEditionBase);
// Register all three schemas — Book first, then the subclasses.
const jt = JsonTology.create({
'baseIri': 'https://bookstore.example',
'schemas': [
BookSchema,
DigitalEditionBase,
PhysicalEditionSchema
] as const
});
// TBox carries owl:disjointWith on PhysicalEdition pointing at DigitalEdition.
const tbox = JSON.parse(jt.toTbox().jsonLd()) as {
'@graph'?: ReadonlyArray<Record<string, unknown>>;
};
const graph = tbox['@graph'] ?? [];
const physicalNode = graph.find((node) => {
return node['@id'] === PhysicalEditionSchema.$id;
});
console.assert(physicalNode !== undefined, 'PhysicalEdition appears in TBox');
const disjointWith = physicalNode?.[OWL.disjointWith] as undefined | { '@id': string };
console.assert(
disjointWith !== undefined,
'PhysicalEdition carries owl:disjointWith'
);
console.assert(
disjointWith?.['@id'] === DigitalEditionBase.$id,
'owl:disjointWith points to DigitalEdition'
);
console.log(
'PhysicalEdition owl:disjointWith DigitalEdition:',
disjointWith?.['@id'] === DigitalEditionBase.$id
);
Example 3: complement class
/**
* Compose.complementOf — Example: AvailableBook as the complement of UnavailableBook
* Demonstrates: complementOf emits owl:complementOf in the OWL TBox.
*
* UnavailableBookSchema is a Book subclass with a fixed `available: false`
* discriminant. AvailableBookSchema is declared as its complement — any Book
* that is NOT UnavailableBook.
*
* The TBox JSON-LD carries:
* urn:bookstore:AvailableBook owl:complementOf urn:bookstore:UnavailableBook
*/
import {
Compose, JsonTology
} from '../../../src/index.js';
import { OWL } from '../../../src/constants/IRI.js';
import { BookSchema } from '../bookstore/index.js';
// UnavailableBook — a Book whose `available` flag is false.
const UnavailableBookSchema = Compose.subClassOf(BookSchema, {
'$id': 'urn:bookstore:UnavailableBook',
'properties': {
'available': {
'const': false,
'type': 'boolean'
}
},
'required': ['available'],
'type': 'object'
} as const);
// AvailableBook — the complement of UnavailableBook, bounded to the Book universe.
const AvailableBookSchema = Compose.complementOf(UnavailableBookSchema, {
'$id': 'urn:bookstore:AvailableBook',
'allOf': [{ '$ref': BookSchema.$id }],
'type': 'object'
} as const);
// Register all schemas — Book first, then the subclasses.
const jt = JsonTology.create({
'baseIri': 'https://bookstore.example',
'schemas': [
BookSchema,
UnavailableBookSchema,
AvailableBookSchema
] as const
});
// TBox carries owl:complementOf on AvailableBook pointing at UnavailableBook.
const tbox = JSON.parse(jt.toTbox().jsonLd()) as {
'@graph'?: ReadonlyArray<Record<string, unknown>>;
};
const graph = tbox['@graph'] ?? [];
const availableNode = graph.find((node) => {
return node['@id'] === AvailableBookSchema.$id;
});
console.assert(availableNode !== undefined, 'AvailableBook appears in TBox');
const complementOf = availableNode?.[OWL.complementOf] as undefined | { '@id': string };
console.assert(
complementOf !== undefined,
'AvailableBook carries owl:complementOf'
);
console.assert(
complementOf?.['@id'] === UnavailableBookSchema.$id,
'owl:complementOf points to UnavailableBook'
);
console.log(
'AvailableBook owl:complementOf UnavailableBook:',
complementOf?.['@id'] === UnavailableBookSchema.$id
);
Bookstore domain examples
Example 1: single-parent subclass (EBook)
EBook is a Book with three extra fields. The full source lives in the shared bookstore domain:
import { Compose } from '../../../../src/index.js';
import { BookSchema } from './Book.js';
/**
* EBook — taxonomic subclass of Book via Compose.subClassOf.
*
* Demonstrates:
* - Compose.subClassOf with a single parent (rdfs:subClassOf in TBox)
* - Format-specific properties layered onto the parent, all via named $refs
* - Generalised if/then/else inference: when fileFormat === 'epub' the then
* branch narrows to require epubVersion; when not-epub (else) pdfVersion is required.
*
* The if clause uses a single const-discriminated property (fileFormat) so
* IfNarrowingObjectType fires and the inferred type is a discriminated union:
* | (EBook base & { fileFormat: 'epub'; epubVersion: string })
* | (EBook base & { pdfVersion: string })
*
* Wire shape: { $id, allOf: [{ $ref: 'urn:bookstore:Book' }, { type: 'object', ... }] }
*
* All properties reference named primitives (strict-graph compliant):
* - downloadUrl → DownloadUrl (format: uri string)
* - fileFormat → EBookFormat (enum: epub | pdf | mobi)
* - fileSizeBytes → FileSizeBytes (non-negative integer)
*/
export const EBookSchema = Compose.subClassOf(BookSchema, {
'$id': 'urn:bookstore:EBook',
'else': {
'properties': { 'pdfVersion': { 'type': 'string' } },
'required': ['pdfVersion'],
'type': 'object'
},
'if': {
'properties': { 'fileFormat': { 'const': 'epub' } },
'required': ['fileFormat'],
'type': 'object'
},
'properties': {
'downloadUrl': { '$ref': 'urn:bookstore:DownloadUrl' },
'fileFormat': { '$ref': 'urn:bookstore:EBookFormat' },
'fileSizeBytes': { '$ref': 'urn:bookstore:FileSizeBytes' }
},
'required': [
'fileFormat',
'downloadUrl'
],
'then': {
'properties': { 'epubVersion': { 'type': 'string' } },
'required': ['epubVersion'],
'type': 'object'
},
'type': 'object'
} as const);Output wire shape:
// {
// $id: 'urn:bookstore:EBook',
// allOf: [
// { $ref: 'urn:bookstore:Book' },
// { type: 'object', properties: { fileFormat: {...}, downloadUrl: {...}, fileSizeBytes: {...} } }
// ]
// }Example 2: subclass + invariant for axioms TypeScript can't express
A SignedFirstEdition is a RareBook whose sole author signed the copy. The structural OWL contract ("subclass of RareBook, adds signedBy and provenance") is expressed by single-parent subClassOf. The "exactly one author" axiom is registered as a runtime invariant (signedFirstEditionIsSoloAuthored) on the schema, surfaced through the same ValidationErrors shape as structural errors. Single-authorship is a predicate over authors, not a distinct OWL class identity, so it deliberately stays out of the TBox.
import { Compose } from '../../../../src/index.js';
import { AuthorNameSchema } from './AuthorName.js';
import { RareBookSchema } from './RareBook.js';
/**
* SignedFirstEdition — a RareBook signed by its sole author.
*
* Single-parent `subClassOf(RareBook)`: structurally adds `signedBy` and
* `provenance`. The "exactly one author" axiom is enforced by the
* registered invariant `signedFirstEditionIsSoloAuthored` (in
* `index.ts`), which fires on every `validate()` / `instantiate()` and
* surfaces in `ValidationErrors` with `keyword: 'jt:invariant'` — same
* collection shape as structural errors. This is how json-tology
* augments TypeScript: cross-field rules ride alongside the schema and
* are projected through type inference, rather than left to ad-hoc
* runtime helpers.
*
* Demonstrates:
* - `Compose.subClassOf(parent, body)` — single-parent shape
* - `$ref` to `AuthorName` for the signature attribution
* - `$ref` to `Provenance` for the custody trail (strict-graph compliant)
* - Pairing an OWL subclass declaration with a registered invariant
* for the rule TypeScript / JSON Schema can't express structurally.
*/
export const SignedFirstEditionSchema = Compose.subClassOf(
RareBookSchema,
{
'$id': 'urn:bookstore:SignedFirstEdition',
'properties': {
'provenance': { '$ref': 'urn:bookstore:Provenance' },
'signedBy': { '$ref': AuthorNameSchema.$id }
},
'required': ['signedBy'],
'type': 'object'
} as const
);Example 3: disjoint classes
PrintBook is a Book that cannot also be an EBook. A single book copy is either a physical artefact or a digital download, never both at once.
import { Compose } from '../../../../src/index.js';
import { BookSchema } from './Book.js';
import { EBookSchema } from './EBook.js';
/**
* PrintBook — physical-format subclass of Book.
*
* Demonstrates:
* - Compose.subClassOf for the taxonomic narrowing
* - Compose.disjointWith asserting PrintBook and EBook share no instances
*
* The disjointWith axiom emits `urn:bookstore:PrintBook owl:disjointWith
* urn:bookstore:EBook` in the OWL TBox — a single book copy is either a
* physical artefact or a digital download, never both at once.
*
* All properties reference named primitives (strict-graph compliant):
* - binding → BindingType (enum: hardcover | paperback)
* - pageCount → PrintPageCount (positive integer)
* - weightGrams → WeightGrams (non-negative number)
*/
const PrintBookBase = Compose.subClassOf(BookSchema, {
'$id': 'urn:bookstore:PrintBook',
'properties': {
'binding': { '$ref': 'urn:bookstore:BindingType' },
'pageCount': { '$ref': 'urn:bookstore:PrintPageCount' },
'weightGrams': { '$ref': 'urn:bookstore:WeightGrams' }
},
'required': [
'binding',
'pageCount'
],
'type': 'object'
} as const);
export const PrintBookSchema = Compose.disjointWith(EBookSchema, PrintBookBase);In the OWL TBox:
urn:bookstore:PrintBook owl:disjointWith urn:bookstore:EBook .Example 4: complement class
OutOfPrintBook is the negation of InPrintBook, bounded to the Book universe via the body's allOf: [{ $ref: Book }].
import { Compose } from '../../../../src/index.js';
import { BookSchema } from './Book.js';
import { InPrintBookSchema } from './InPrintBook.js';
/**
* OutOfPrintBook — the OWL complement of InPrintBookSchema, bounded to the
* Book universe via `allOf + $ref(Book)`. Covers any book whose
* `printStatus` is not `'inPrint'` (so: `'outOfPrint'` or `'limitedRun'`
* editions that have sold through).
*
* Demonstrates `Compose.complementOf` with a body that already carries
* `allOf` so the result has both `not` (the OWL complement) and `allOf`
* (the structural subclass) at the top level. The OWL projection emits:
*
* urn:bookstore:OutOfPrintBook owl:complementOf urn:bookstore:InPrintBook .
* urn:bookstore:OutOfPrintBook rdfs:subClassOf urn:bookstore:Book .
*
* Wire shape:
* {
* $id: 'urn:bookstore:OutOfPrintBook',
* not: { $ref: 'urn:bookstore:InPrintBook' },
* allOf: [{ $ref: 'urn:bookstore:Book' }],
* type: 'object'
* }
*
* JSON Schema runtime: validates as `Book AND NOT InPrintBook` — only Book-
* shaped values that fail the InPrintBook constraint pass.
*/
export const OutOfPrintBookSchema = Compose.complementOf(InPrintBookSchema, {
'$id': 'urn:bookstore:OutOfPrintBook',
'allOf': [{ '$ref': BookSchema.$id }],
'type': 'object'
} as const);In the OWL TBox:
urn:bookstore:OutOfPrintBook owl:complementOf urn:bookstore:InPrintBook .
urn:bookstore:OutOfPrintBook rdfs:subClassOf urn:bookstore:Book .JSON Schema runtime: validates as Book AND NOT InPrintBook; only Book-shaped values that fail the InPrintBook constraint pass.
Comparison
const EBook = Compose.subClassOf(BookSchema, {
$id: 'urn:bookstore:EBook',
type: 'object',
properties: { fileFormat: { type: 'string', enum: ['epub', 'pdf', 'mobi'] } },
});// Zod has no native subclass concept; structural extension is the closest analog.
const EBook = BookSchema.extend({ fileFormat: z.enum(['epub', 'pdf', 'mobi']) });
// Limitation: no OWL TBox emission, no multi-parent support.import { Schema } from 'effect';
const EBook = Schema.extend(BookSchema, Schema.Struct({ fileFormat: Schema.Literal('epub', 'pdf', 'mobi') }));
// Limitation: no taxonomic vs property-merge distinction.import { Type } from '@sinclair/typebox';
const EBook = Type.Composite([
BookSchema,
Type.Object({ fileFormat: Type.Union([Type.Literal('epub'), Type.Literal('pdf'), Type.Literal('mobi')]) }),
]);
// Limitation: no semantic distinction between extension and subclassing.import * as t from 'io-ts';
const EBook = t.intersection([BookCodec, t.type({ fileFormat: t.union([t.literal('epub'), t.literal('pdf'), t.literal('mobi')]) })]);
// Limitation: structural intersection only; no class identity.import * as v from 'valibot';
const EBook = v.intersect([
BookSchema,
v.object({ fileFormat: v.picklist(['epub', 'pdf', 'mobi']) }),
]);
// Limitation: no inheritance model; no OWL output.class EBook(Book):
file_format: Literal['epub', 'pdf', 'mobi']
# Pydantic's Python class hierarchy is the natural taxonomic mechanism,
# but it does not emit OWL or JSON Schema axioms by default.// Limitation: feature not directly supported in AJV. See /comparisons for the matrix.// Limitation: feature not directly supported in Yup. See /comparisons for the matrix.// Limitation: feature not directly supported in Joi. See /comparisons for the matrix.// Limitation: feature not directly supported in ArkType. See /comparisons for the matrix.// Limitation: feature not directly supported in Runtypes. See /comparisons for the matrix.Related
Compose.extend- property-merging extension (single parent, allOf+$ref shape, no explicit "subclass" semantic)Compose.equivalent-owl:equivalentClassfor structurally identical typesCompose.intersection- genericallOfover multiple schemas