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);
$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.');
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);
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).');
$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 asbaseIritoJsonTology.createso relative$refvalues resolve correctly. - Path segment - use the domain entity name as the path (
/Book,/Customer,/OrderLine). One schema per IRI. - Stability - once a schema
$idis published and referenced by other schemas, treat it as stable. Changing a$idbreaks all cross-schema$refthat 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:
InferTypeflags any$refthat 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/cleanagainst it) and throwsGraphErrorwith codeREF_UNRESOLVEDif any non-fragment$refpoints to an IRI that is neither in the registry nor embedded as a nested$idwithin 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.');
See Error class hierarchy for the full GraphError surface.
Registry methods
| Method | Description |
|---|---|
set / registerAnonymous | Add schemas to the runtime |
registry.has | Check if a schema is registered |
registry.get | Retrieve the original schema object |
registry.keys | Enumerate all registered $id values |
toSchema | Reconstruct a schema from the canonical graph |
See also
- Bookstore domain - where all six schemas are registered
- Composition - derive new schemas to register
- Validation modes - enforcement layer reference
- Argument conventions - how registered schemas work as
SchemaRef - Runtime decoding across packages - registering a hash-namespace schema with CURIE
$refs in one package and consuming it, typed, from another - jt: keywords - json-tology-specific schema extensions