Skip to content

Schemas

Schemas are plain JSON Schema objects with $id and as const. The registry stores them, compiles a canonical validation graph for each, and exposes lookup methods. json-tology targets JSON Schema draft 2020-12 (https://json-schema.org/draft/2020-12/schema), the dialect on track for Proposed Standard via the IETF JSON Schema Working Group.

All examples use the bookstore domain. See Getting Started for installation and the basic JsonTology.create() call.


Schema authoring

Schemas are declared as TypeScript const objects so the compiler can read the literal types. The minimal shape is:

/**
 * Schema authoring — Example 7: minimal schema with $id and as const
 *
 * Schemas are plain JSON Schema objects. `$id` is required and must be a
 * fully-qualified IRI. `as const` is required so TypeScript preserves the
 * literal types that `InferType<T>` reads.
 *
 * The canonical bookstore uses `urn:bookstore:` IRIs. This example
 * demonstrates the minimal shape with bookstore characters as data.
 */

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

// CustomerSchema follows the minimal shape: $id, type, properties, required.
const id: string = CustomerSchema.$id;

console.assert(id === 'urn:bookstore:Customer');

const schemaType: string = CustomerSchema.type;

console.assert(schemaType === 'object');
console.assert(Array.isArray(CustomerSchema.required));

// Validate Bastian Balthazar Bux against the registered schema.
const errs = bookstoreEntities.validate(CustomerSchema.$id, {
  'addresses': [],
  'customerId': 'c1a2b3d4-e5f6-7890-abcd-ef1234567890',
  'email': 'bastian.bux@bookstore.example',
  'name': 'Bastian Balthazar Bux'
});

console.assert(errs.length === 0);

console.log('CustomerSchema.$id:', id);
console.log('CustomerSchema.type:', schemaType);
console.log('Validation errors (expect 0):', errs.length);
Output
Press Execute to run this example against the real library.

$id is required. Every schema registered with set() must carry a fully-qualified IRI as its $id. The IRI is the stable identity used by registry.has, registry.get, validate, instantiate, materialize, and cross-schema $ref. Use the project's baseIri as the namespace:

/**
 * JsonTology.create — Example 8: register multiple schemas at once
 *
 * `JsonTology.create` takes a `schemas` array (as const) and a `baseIri`.
 * All schemas are registered, the validation graph is compiled, and the
 * type map is built. `$ref` resolution works because all referenced
 * schemas are registered in the same call.
 */

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

// bookstoreEntities was created with all 31 schemas pre-registered.
// Validate a concrete address — if AddressSchema is not registered
// the call would throw REF_UNRESOLVED, so zero errors proves presence.
const addressErrs = bookstoreEntities.validate(AddressSchema.$id, {
  'city': 'München',
  'country': 'DE',
  'postalCode': '80331',
  'street': 'Reichenbachstraße 14'
});

console.assert(addressErrs.length === 0);

// Validate the canonical fixtures to confirm the other schemas are registered.
const customerErrs = bookstoreEntities.validate(CustomerSchema.$id, {
  'addresses': [],
  'customerId': 'c1a2b3d4-e5f6-7890-abcd-ef1234567890',
  'email': 'bastian.bux@bookstore.example',
  'name': 'Bastian Balthazar Bux'
});

console.assert(customerErrs.length === 0);

const bookErrs = bookstoreEntities.validate(BookSchema.$id, {
  'authors': ['Michael Ende'],
  'inStock': true,
  'isbn': '9783522128001',
  'price': {
    'amount': 850,
    'currency': 'EUR'
  },
  'printStatus': 'outOfPrint',
  'publishedOn': '1979-09-01',
  'stockLevel': 5,
  'title': 'Die unendliche Geschichte'
});

console.assert(bookErrs.length === 0);

console.log('AddressSchema.$id:', AddressSchema.$id, '— address validation errors:', addressErrs.length);
console.log('CustomerSchema.$id:', CustomerSchema.$id, '— customer validation errors:', customerErrs.length);
console.log('BookSchema.$id:', BookSchema.$id, '— book validation errors:', bookErrs.length);
console.log('All three schemas registered in bookstoreEntities — cross-schema $ref resolution active.');
Output
Press Execute to run this example against the real library.

as const is required for type inference. Without it, TypeScript widens string literals to string and InferType cannot derive precise property types.


$ref: cross-schema references

Use $ref to point one schema at another by IRI. The runtime resolves the reference against the registry.

/**
 * $ref cross-schema — Example 9: references resolved through the registry
 *
 * A `$ref` points one schema at another by IRI. The runtime resolves it
 * against the registry on first use. Cross-schema `$ref` must point to a
 * `$id` that is registered (or nested within a registered schema), or the
 * runtime throws `GraphError` with code `REF_UNRESOLVED`.
 *
 * `OrderLineSchema` references `IsbnSchema` and `MoneySchema` via `$ref`.
 * This example validates a concrete order line against the registered
 * schema so the resolver exercises the cross-schema path.
 */

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

// The fixture order line has bookIsbn and unitPrice — both are $ref fields.
const line = aboxFixtures.order.orderLines[0];
const errs = bookstoreEntities.validate(OrderLineSchema.$id, line);

console.assert(errs.length === 0);

const bookIsbn: string = line.bookIsbn;
const quantity: number = line.quantity;

console.assert(bookIsbn === '9783522128001');
console.assert(quantity === 1);

console.log('OrderLineSchema.$id:', OrderLineSchema.$id);
console.log('line.bookIsbn ($ref -> IsbnSchema):', bookIsbn);
console.log('line.quantity:', quantity);
console.log('line.unitPrice ($ref -> MoneySchema):', line.unitPrice);
console.log('Validation errors (expect 0):', errs.length);
Output
Press Execute to run this example against the real library.

Local fragment refs (#, #/properties/foo, #anchor) resolve within the same schema document and do not require registry lookup.

Cross-schema non-fragment refs must point to a $id that is registered (or nested within a registered schema). See the strict resolution section below for how enforcement works.


$defs and anchors

Use $defs to define reusable sub-schemas inline within a parent schema. They are accessible via $ref with a JSON Pointer fragment (#/$defs/Name) or via a named $anchor.

/**
 * $defs and $anchor — Example 10: inline sub-schemas and named anchors
 *
 * `$defs` defines reusable sub-schemas inline within a parent schema.
 * They are accessible via `$ref` with a JSON Pointer fragment
 * (`#/$defs/Name`) or via a named `$anchor`.
 *
 * This example uses a focused one-shot registry so the `$defs`/`$anchor`
 * path is exercised directly and the assertion is self-contained.
 */

import { JsonTology } from '../../../src/index.js';

const OrderSchema = {
  '$defs': {
    'ShippingNote': {
      '$anchor': 'shipping-note',
      'minLength': 1,
      'type': 'string'
    },
    'Status': {
      'enum': [
        'pending',
        'shipped',
        'delivered',
        'cancelled'
      ],
      'type': 'string'
    }
  },
  '$id': 'urn:docs-schemas-10:Order',
  'properties': {
    'id': { 'type': 'string' },
    'note': { '$ref': '#shipping-note' },
    'status': { '$ref': '#/$defs/Status' }
  },
  'required': [
    'id',
    'status'
  ],
  'type': 'object'
} as const;

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

// Valid order — status from enum, note optional.
const errsOk = jt.validate(OrderSchema.$id, {
  'id': '09f8e7d6-c5b4-3210-9876-543210fedcba',
  'note': 'Bastian Balthazar Bux — handle with care',
  'status': 'pending'
});

console.assert(errsOk.length === 0);

// Invalid status — not in enum.
const errsBad = jt.validate(OrderSchema.$id, {
  'id': '09f8e7d6-c5b4-3210-9876-543210fedcba',
  'status': 'unknown-status'
});

console.assert(errsBad.length > 0);

console.log('valid order (status from $defs/Status enum, note via #shipping-note anchor) — errors:', errsOk.length);
console.log('invalid status ("unknown-status" not in enum) — errors:', errsBad.length);
console.log('$defs sub-schemas accessible via JSON Pointer (#/$defs/Status) and anchor (#shipping-note).');
Output
Press Execute to run this example against the real library.

$id conventions

Use fully-qualified IRIs as schema identifiers:

  • Base IRI - use the same origin for all schemas in a project (https://bookstore.example). Pass it as baseIri to JsonTology.create so relative $ref values resolve correctly.
  • Path segment - use the domain entity name as the path (/Book, /Customer, /OrderLine). One schema per IRI.
  • Stability - once a schema $id is published and referenced by other schemas, treat it as stable. Changing a $id breaks all cross-schema $ref that target it.

IRI-based identity is what allows the runtime to perform $ref resolution, compile-time type checking, and ontology export without additional configuration.


Cross-schema $ref strict resolution Compile-time + Runtime

Cross-schema $ref resolution is enforced at both layers:

  • Compile-time: InferType flags any $ref that points to an IRI not present in the type map at the call site.
  • Runtime: The registry performs a lazy walk on first use of an entry (the first validate / instantiate / materialize / create / convert / cast / is / clean against it) and throws GraphError with code REF_UNRESOLVED if any non-fragment $ref points to an IRI that is neither in the registry nor embedded as a nested $id within the same schema.

Local fragment refs (#, #/foo, #anchor) are unaffected by the strict check.

The walk runs at most once per schema entry - subsequent calls against the same schema use the cached result.

/**
 * $ref strict resolution — Example 11: GraphError REF_UNRESOLVED
 *
 * If a cross-schema `$ref` points to an IRI not present in the registry,
 * the runtime throws `GraphError` with code `REF_UNRESOLVED` on the first
 * use of the referencing schema. The lazy walk runs at most once per
 * schema entry; subsequent calls use the cached (error) result.
 *
 * Local fragment refs (`#`, `#/foo`, `#anchor`) are unaffected.
 */

import {
  GraphError, JsonTology
} from '../../../src/index.js';

const OrderLineSchema = {
  '$id': 'urn:docs-schemas-11:OrderLine',
  'properties': {
    // This $ref points to a schema that is NOT registered in this registry.
    'book': { '$ref': 'urn:docs-schemas-11:Book' },
    'qty': {
      'minimum': 1,
      'type': 'integer'
    }
  },
  'required': ['qty'],
  'type': 'object'
} as const;

// BookSchema is intentionally NOT registered.
const jt = JsonTology.create({
  'baseIri': 'urn:docs-schemas-11',
  // doc example with synthetic fixture schemas
  'enableStrictGraph': false,
  'schemas': [OrderLineSchema] as const
});

let caught = false;
let errorCode = '';

try {
  jt.validate(OrderLineSchema.$id, {
    'book': {},
    'qty': 1
  });
} catch (error) {
  if (error instanceof GraphError && error.code === 'REF_UNRESOLVED') {
    caught = true;
    errorCode = error.code;
  }
}

console.assert(caught);

console.log('Unresolved $ref throws GraphError — caught:', caught);
console.log('error.code:', errorCode);
console.log('$ref to urn:docs-schemas-11:Book raises REF_UNRESOLVED because BookSchema was not registered.');
Output
Press Execute to run this example against the real library.

See Error class hierarchy for the full GraphError surface.


Registry methods

MethodDescription
set / registerAnonymousAdd schemas to the runtime
registry.hasCheck if a schema is registered
registry.getRetrieve the original schema object
registry.keysEnumerate all registered $id values
toSchemaReconstruct a schema from the canonical graph

See also

Released under the MIT License.