Skip to content

Graph Concepts

Validation modes: Validation modes reference

json-tology represents every schema as a node in a directed graph. This page explains the key concepts of that graph model - what lives where, how relationships work, and how standard semantic web vocabulary maps onto JSON Schema constructs.

TBox vs ABox

The semantic web distinguishes two kinds of knowledge:

  • TBox (Terminological Box) - the schema layer: class declarations, property declarations, domain and range constraints. Describes the shape of the world.
  • ABox (Assertional Box) - the data layer: typed individuals, property assertions. Describes instances of that shape.

In json-tology:

/**
 * TBox vs ABox — schema layer vs data layer.
 *
 * The TBox (Terminological Box) describes the shape of the world: class
 * declarations, property declarations, domain and range. The ABox
 * (Assertional Box) describes instances: typed individuals, property values.
 *
 * `toTbox()` emits the TBox; `toQuads(schema, data)` emits the ABox for a
 * given instance; `fromQuads(schemaId, quads)` lifts ABox quads back to
 * typed objects.
 *
 * Demonstrates: TBox output, ABox quad projection, and fromQuads round-trip
 * using the bookstore Customer schema.
 */

import {
  aboxFixtures, bookstoreEntities, CustomerSchema
} from '../bookstore/index.js';

// TBox — OWL class + property declarations for all registered schemas
const tbox = bookstoreEntities.toTbox();

console.assert(typeof tbox.jsonLd() === 'string', 'TBox emits JSON-LD string');

// ABox — RDF quads about a specific customer instance
const abox = bookstoreEntities.toQuads(CustomerSchema, aboxFixtures.customer);

console.assert(abox.length > 0, 'ABox quad projection emits quads');

// fromQuads — lift ABox quads back to typed Customer objects
// Use the string key form for full type inference on the returned array.
const recovered = bookstoreEntities.fromQuads(CustomerSchema.$id, abox);

console.assert(recovered.length > 0, 'fromQuads recovers at least one Customer');

const first = recovered[0];

if (first === undefined) {
  throw new Error('expected recovered customer');
}

console.assert(
  first.email === aboxFixtures.customer.email,
  'recovered Customer has correct email'
);

console.log('TBox/ABox: ABox quads:', abox.length, '| fromQuads recovered:', recovered.length, '| email:', recovered[0]?.email);
Output
Press Execute to run this example against the real library.

entities.toTbox() emits the TBox. entities.toQuads(schema, data) emits the ABox for a given data instance. entities.fromQuads(schemaId, quads) lifts ABox quads back to typed objects.

Bookstore example:

  • BookSchema lives in the TBox - it describes the class urn:bookstore:Book.
  • { isbn: '9780140449136', title: 'The Odyssey', authors: [...], ... } is an ABox assertion about a specific individual of that class.

Open-world assumption

JSON Schema describes what is required and constrained, not what is exhaustively listed. This is the open-world assumption (OWA): a schema does not claim to enumerate all properties that may ever exist on an instance.

/**
 * Open-world assumption — schemas constrain, not enumerate.
 *
 * JSON Schema (and json-tology) follow the open-world assumption (OWA): a
 * schema defines what is required and constrained, not what is exhaustively
 * allowed. Additional properties are permitted unless `additionalProperties`
 * or `jt:config.extra` explicitly restricts them.
 *
 * CustomerSchema requires `id`, `email`, and `name` but does not forbid
 * additional properties by default. An instance with an extra `loyaltyTier`
 * field still validates.
 *
 * Demonstrates: OWA — extra properties pass by default; additional field
 * does not cause validation failure.
 */

import {
  bookstoreEntities,
  CustomerSchema
} from '../bookstore/index.js';

// Valid: includes all required fields
const baseCustomer = {
  'addresses': [],
  'customerId': 'c1a2b3d4-e5f6-7890-abcd-ef1234567890',
  'email': 'bastian.bux@bookstore.example',
  'name': 'Bastian Balthazar Bux'
};

const baseResult = bookstoreEntities.validate(CustomerSchema.$id, baseCustomer);

// ok is true when the ValidationErrors collection is empty (no errors)
console.assert(baseResult.ok, 'base customer is valid');

// OWA: extra property `loyaltyTier` is allowed (no additionalProperties: false)
const extendedCustomer = {
  ...baseCustomer,
  // unknown to the schema — allowed by OWA
  'loyaltyTier': 'gold'
};

const extendedResult = bookstoreEntities.validate(CustomerSchema.$id, extendedCustomer);

// Under OWA the extra property does not cause a validation failure
console.assert(extendedResult.ok, 'extra property is allowed under open-world assumption');

console.log('OWA: base valid:', baseResult.ok, '| with extra property:', extendedResult.ok);
Output
Press Execute to run this example against the real library.

This schema says: every Customer must have id, email, and name. It does not say those are the only properties allowed.

Whether additional properties are permitted depends on additionalProperties (JSON Schema) or jt:config.extra (json-tology extension):

SettingBehavior
additionalProperties omitted (default)Additional properties allowed
additionalProperties: falseAdditional properties rejected
jt:config.extra: 'allow'Additional properties passed through silently
jt:config.extra: 'forbid'Additional properties raise a validation error
jt:config.extra: 'ignore'Additional properties stripped from output

Contrast with closed-world (Pydantic-style): A Pydantic model lists all fields and rejects extras unless extra='allow' is set. Pydantic's default is closed-world for extras. JSON Schema's default is open-world. json-tology follows JSON Schema's convention - the OWA is the default unless you explicitly restrict it.


Specificity and rdfs:subClassOf

More constraints = more specific type = subclass.

Every PremiumCustomer schema that extends Customer describes a narrower set of valid instances. In OWL terms: the class PremiumCustomer is a subclass of Customer.

/**
 * Specificity and rdfs:subClassOf — Compose.extend produces a subclass.
 *
 * `Compose.extend(parent, additions)` produces an `allOf + $ref` schema: the
 * parent is referenced via `$ref` and the additions live in a second `allOf`
 * member. The OWL projection emits `rdfs:subClassOf` for the extended schema.
 *
 * The new `PremiumCustomer` subclass registers onto the canonical
 * `bookstoreEntities` registry so it inherits every existing
 * transitive `$ref` (Email, Address, …).
 *
 * Demonstrates: Compose.extend on CustomerSchema to model a PremiumCustomer
 * subclass; TBox JSON-LD carries both class IRIs.
 */

import { Compose } from '../../../src/index.js';
import {
  bookstoreEntities,
  CustomerSchema
} from '../bookstore/index.js';

// PremiumCustomer extends Customer with a `tier` property.
const PremiumCustomerSchema = Compose.extend(
  CustomerSchema,
  {
    'properties': {
      'tier': {
        'enum': [
          'gold',
          'platinum'
        ],
        'type': 'string'
      }
    },
    'required': ['tier']
  } as const,
  'urn:bookstore:PremiumCustomer'
);

// Capture the widened instance so PremiumCustomer is a known `$id` key — the
// precise string-id `validate` form resolves it without re-checking the
// `Compose.extend` schema object against the document-type constraint.
const withPremium = bookstoreEntities.set(PremiumCustomerSchema);

// Both classes appear in the TBox.
const tboxJson = bookstoreEntities.toTbox().jsonLd();

console.assert(tboxJson.includes(CustomerSchema.$id), 'Customer class in TBox');
console.assert(
  tboxJson.includes(PremiumCustomerSchema.$id),
  'PremiumCustomer subclass in TBox'
);

// A valid PremiumCustomer validates against its own schema.
const result = withPremium.validate(PremiumCustomerSchema.$id, {
  'addresses': [],
  'customerId': 'a1b2c3d4-e5f6-4890-abcd-ef1234567890',
  'email': 'cornelia.funke@bookstore.example',
  'name': 'Cornelia Funke',
  'tier': 'gold'
});

// ok is true when the ValidationErrors collection is empty (no errors).
console.assert(result.ok, 'valid PremiumCustomer passes validation');

console.log('Customer in TBox:', tboxJson.includes(CustomerSchema.$id), '| PremiumCustomer subclass valid:', result.ok);
Output
Press Execute to run this example against the real library.

In the TBox, this emits:

turtle
urn:bookstore:PremiumCustomer rdfs:subClassOf urn:bookstore:Customer .

Compose.extend() produces an allOf + $ref shape: the parent is referenced via $ref and the additions live in a second allOf member. This preserves the merged type at compile time and maps cleanly to rdfs:subClassOf in the graph.

Design pattern - "author the most common ancestor first": Define the base schema first, then layer specializations with Compose.extend(). This keeps the subclass hierarchy explicit and the TBox traversable.

See Compose.extend for full API documentation.


Equivalence and owl:equivalentClass

Two schemas can describe structurally identical data while carrying domain-distinct names.

/**
 * Equivalence and owl:equivalentClass — Compose.equivalent produces a name alias.
 *
 * `Compose.equivalent(source, additions)` creates a thin `$ref` alias schema.
 * The OWL projection emits `owl:equivalentClass` for the two class IRIs.
 * Instances satisfying the source also satisfy the alias and vice versa.
 *
 * Demonstrates: Compose.equivalent on IsbnSchema to model a PrimaryIsbn alias;
 * TBox carries both class IRIs and both validate the same data.
 */

import {
  Compose,
  JsonTology
} from '../../../src/index.js';
import { IsbnSchema } from '../bookstore/index.js';

// PrimaryIsbn is a domain-distinct alias for Isbn — same validation, distinct name
const PrimaryIsbnSchema = Compose.equivalent(IsbnSchema, {
  '$id': 'urn:bookstore:PrimaryIsbn',
  'description': 'The canonical ISBN used for catalog indexing'
});

// doc example with synthetic fixture schemas (strict-graph default does not throw because no inline duplicates)
const jt = JsonTology.create({
  'baseIri': 'https://bookstore.example',
  'schemas': [
    IsbnSchema,
    PrimaryIsbnSchema
  ] as const
});

// Both validate the same valid ISBN-13 for Die unendliche Geschichte
const isbn = '9783522128001';

const isbnResult = jt.validate(IsbnSchema.$id, isbn);
const primaryResult = jt.validate(PrimaryIsbnSchema.$id, isbn);

// ok is true when the ValidationErrors collection is empty (no errors)
console.assert(isbnResult.ok, 'Isbn validates the ISBN');
console.assert(primaryResult.ok, 'PrimaryIsbn (equivalent) validates the same ISBN');

// Both reject invalid data
const invalid = 'not-an-isbn';
const isbnInvalid = jt.validate(IsbnSchema.$id, invalid);
const primaryInvalid = jt.validate(PrimaryIsbnSchema.$id, invalid);

console.assert(!isbnInvalid.ok, 'Isbn rejects invalid input');
console.assert(!primaryInvalid.ok, 'PrimaryIsbn (equivalent) also rejects invalid input');

console.log('equivalentClass: Isbn valid:', isbnResult.ok, '| PrimaryIsbn valid:', primaryResult.ok, '| both reject invalid:', !isbnInvalid.ok && !primaryInvalid.ok);
Output
Press Execute to run this example against the real library.

In the TBox, this emits:

turtle
urn:bookstore:PrimaryIsbn owl:equivalentClass urn:bookstore:Isbn .

Compose.equivalent() creates a thin $ref alias. Instances that satisfy Isbn also satisfy PrimaryIsbn and vice versa - they are logically interchangeable.

Use case: when a domain concept needs a distinct name for clarity but shares an existing structure. For example, OrderId and ReturnOrderId might share the same string pattern but represent different domain concepts. Equivalence avoids structural duplication while keeping names meaningful.


See also Graph internals for $ref resolution, serializer behavior, ABox projection, $id conventions, and the irreducible jt:* set.

See also

Released under the MIT License.