Skip to content

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.

ts
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. Emits rdfs:subClassOf per parent in the OWL TBox.
  • disjointWith - two classes share no instances (e.g. Weapon and Armor). Emits owl:disjointWith.
  • complementOf - a class is the negation of another (e.g. NonHumanRace is everything that is not HumanRace). Emits owl:complementOf.

Don't use this when

  • You only need property-merging with a single parent - use Compose.extend, which is structurally equivalent (both produce allOf + $ref) but signals "extension" rather than "is-a".
  • You want type aliasing without OWL semantics - use Compose.equivalent.
  • You want individual-level identity (owl:sameAs between two ABox individuals) - use JsonTology.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'];
  })
);
Output
Press Execute to run this example against the real library.

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

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

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:

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

ts
// {
//   $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.

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

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

turtle
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 }].

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

turtle
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

ts
const EBook = Compose.subClassOf(BookSchema, {
  $id: 'urn:bookstore:EBook',
  type: 'object',
  properties: { fileFormat: { type: 'string', enum: ['epub', 'pdf', 'mobi'] } },
});
ts
// 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.
ts
import { Schema } from 'effect';
const EBook = Schema.extend(BookSchema, Schema.Struct({ fileFormat: Schema.Literal('epub', 'pdf', 'mobi') }));
// Limitation: no taxonomic vs property-merge distinction.
ts
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.
ts
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.
ts
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.
py
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.
ts
// Limitation: feature not directly supported in AJV. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Yup. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Joi. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in ArkType. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Runtypes. See /comparisons for the matrix.

See also

Released under the MIT License.