Skip to content

set, registerAnonymous, registry access

Schema management: adding, inspecting, and introspecting the schema registry.


JsonTology.set

Declaration. Adds or replaces one or more schemas and returns this with the schema types accumulated into the type map. The schema is always the first argument:

  • set(schema): single; key derived from schema.$id.
  • set(schema, iri): explicit key; for non-canonical aliasing.
  • set([schema | [schema, iri], ...]): bulk; each entry is a schema or [schema, iri] tuple.

Schemas with $ref that reference other schemas must have those other schemas in the registry first (or supplied in the same set call). Replaces silently on $id collision, per Map.set semantics.

Returns JsonTology<merged TRefs> so the new schema's static type is visible to subsequent validate / instantiate / is calls.

Use this when you need to add schemas after construction, or when schemas are loaded from files at startup. Prefer JsonTology.create({ schemas }) for known-at-compile-time schemas since it builds the full type map in one pass.

Examples

Example 1: Construction-time registration (preferred)

/**
 * register / has / get / list — Example 1: Registry lifecycle
 * Demonstrates: post-construction register, has/get inspection, registerAnonymous
 *
 * Operates against the canonical bookstore registry. The canonical
 * entities (Customer, Book, …) are already registered at construction
 * time; this example layers a GiftCertificate schema on top to show
 * post-hoc registration.
 */

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

// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();

// Canonical entities registered at construction time.
console.assert(jt.registry.has(CustomerSchema.$id));
console.assert(jt.registry.has(BookSchema.$id));

// Post-construction register — a gift certificate Carl Conrad Coreander
// offers for purchases at the antiquariat.
const CoreanderGiftCertificateSchema = {
  '$id': 'https://bookstore.example/CoreanderGiftCertificate',
  'properties': {
    'code': { 'type': 'string' },
    'value': {
      'maximum': 10_000,
      'minimum': 0,
      'type': 'number'
    }
  },
  'required': [
    'code',
    'value'
  ],
  'type': 'object'
} as const;

console.assert(!jt.registry.has(CoreanderGiftCertificateSchema.$id));

jt.set(CoreanderGiftCertificateSchema);

console.assert(jt.registry.has(CoreanderGiftCertificateSchema.$id));

// Retrieve the schema object.
const raw = jt.registry.get(CoreanderGiftCertificateSchema.$id);

console.assert(raw !== undefined);

// registerAnonymous — no $id needed. Useful for ad-hoc input shapes,
// e.g. a one-off "BastianCheckoutPayload" the storefront posts.
const syntheticId = jt.registerAnonymous({
  'properties': {
    'certificateCode': { 'type': 'string' },
    'remainingBalance': { 'type': 'number' }
  },
  'required': [
    'certificateCode',
    'remainingBalance'
  ],
  'type': 'object'
});

console.assert(typeof syntheticId === 'string' && syntheticId.length > 0);
console.assert(jt.registry.has(syntheticId));

console.log('canonical schemas present:', jt.registry.has(CustomerSchema.$id), jt.registry.has(BookSchema.$id));
console.log('gift certificate registered:', jt.registry.has(CoreanderGiftCertificateSchema.$id));
console.log('anonymous schema id:', syntheticId);
console.log('total registered schemas:', jt.registry.size);
Output
Press Execute to run this example against the real library.

Example 2: Post-construction registration

/**
 * register — Example 2: post-construction registration
 * Demonstrates: set() fluent chaining, array registration
 *
 * Uses a permissive doc-registry so demo schemas with inline shapes
 * can be added without triggering the strict-graph gate.
 */

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

// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();

// Post-construction registration
jt.set(AddressSchema).set(CustomerSchema);

// Or register an array:
jt.set([
  AddressSchema,
  CustomerSchema
] as const);

console.assert(jt.registry.has(AddressSchema.$id), 'AddressSchema should be registered');
console.assert(jt.registry.has(CustomerSchema.$id), 'CustomerSchema should be registered');

console.log('AddressSchema registered:', jt.registry.has(AddressSchema.$id));
console.log('CustomerSchema registered:', jt.registry.has(CustomerSchema.$id));
console.log('registered schema count:', jt.registry.size);
Output
Press Execute to run this example against the real library.

Example 3: Register a composed schema immediately

import { Compose } from '../../../src/index.js';
import {
  BibliographicRecordSchema,
  createBookstoreDocRegistry
} from '../bookstore/index.js';

// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();

// Compose.pick reads the schema's own `properties` key, so picking from flat
// schemas (like BibliographicRecordSchema) works directly. BookSchema is a
// Compose.subClassOf composition (allOf): pick bibliographic fields from
// BibliographicRecordSchema rather than from BookSchema.
const BookSummarySchema = Compose.pick(
  BibliographicRecordSchema,
  [
    'isbn',
    'title',
    'authors'
  ] as const,
  'https://bookstore.example/BookSummary'
);

jt.set(BookSummarySchema);

console.assert(
  jt.registry.has('https://bookstore.example/BookSummary'),
  'BookSummarySchema should be registered'
);

console.log('BookSummary $id:', BookSummarySchema.$id);
console.log('registered in registry:', jt.registry.has(BookSummarySchema.$id));
console.log('picked properties:', Object.keys(BookSummarySchema.properties));
Output
Press Execute to run this example against the real library.

Comparison

ts
jt.set(BookSchema);
// Or: JsonTology.create({ schemas: [...] as const })
ts
// Zod schemas are module-scope  - no registry. Import the schema object directly.
ts
// Limitation: Valibot has no registry concept. Each schema is an isolated
// value; cross-schema reference is structural - import the schema and embed it.
import { BookSchema } from './bookstore.js';
ts
// Limitation: io-ts has no registry concept. Codecs are values exported from
// modules; cross-codec reference is structural via t.intersection / t.union /
// t.recursion (for self-referential cases). No central $id-keyed lookup.
import { BookCodec } from './bookstore.js';
ts
// TypeBox schemas are plain objects  - no registry concept.
ts
ajv.addSchema(bookSchema);
// Or: ajv.addSchema([schema1, schema2])
py
# Pydantic registers via the Python class system automatically.
# No explicit registry call needed.
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 Effect Schema. 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.

JsonTology.registerAnonymous

Declaration. Registers a schema that may not have a $id. If $id is absent, assigns a content-hash-based synthetic ID (urn:json-tology:hash:<hex>). If $id is present, delegates to set. Returns the $id string used for registration.

Use this when you receive schemas from external sources (OpenAPI $defs, dynamic form builders) that may not carry a stable $id.

Examples

import {
  createBookstoreDocRegistry
} from '../bookstore/index.js';

// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();

const syntheticId = jt.registerAnonymous({
  'properties': {
    'couponCode': { 'type': 'string' },
    'discount': {
      'maximum': 1,
      'minimum': 0,
      'type': 'number'
    }
  },
  'required': [
    'couponCode',
    'discount'
  ],
  'type': 'object'
});

console.assert(syntheticId.startsWith('urn:json-tology:hash:'), 'Synthetic ID should be hash-based');

// registerAnonymous returns a runtime-computed hash ID that is not part of the
// registry's compile-time schema-ID union, so validate by passing the ID
// through the underlying registry (which accepts an arbitrary string).
const result = jt.registry.validate(syntheticId, {
  'couponCode': 'SAVE10',
  'discount': 0.1
});

console.assert(result.ok, 'Valid coupon should pass');

const invalid = jt.registry.validate(syntheticId, { 'couponCode': 'SAVE10' });

console.assert(!invalid.ok, 'Missing discount should fail');

console.log('syntheticId:', syntheticId);
console.log('valid coupon result.ok:', result.ok);
console.log('missing discount result.ok:', invalid.ok);
console.log('missing discount errors:', invalid.items.map((err) => {
  return `${err.path}: ${err.message}`;
}));
Output
Press Execute to run this example against the real library.

jt.registry: the Map-native interface

jt.registry mirrors the surface of a native Map<string, Schema>. Reads (has, get, keys, values, entries, forEach, size, for...of) and writes (set, delete, clear) are spelled exactly as on Map. No facade aliases on JsonTology itself; everything goes through the registry.

set requires the key to equal the schema's $id. clear and delete are constant-time. Every mutation bumps jt.registry.revision; callers cache derived views (ontology builders, externally cached graphs) by snapshotting the revision and rebuilding when it advances.

jt.registry.has(iri)

O(1) lookup. Returns true if a schema with the given $id is registered.

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

console.assert(
  bookstoreEntities.registry.has(CustomerSchema.$id),
  'CustomerSchema should be registered'
);
console.assert(
  !bookstoreEntities.registry.has('urn:bookstore:NonExistent'),
  'NonExistent schema should not be registered'
);

// Guard pattern:
function validateIfPresent(schemaId: string, data: unknown) {
  const schema = bookstoreEntities.registry.get(schemaId);

  if (!schema) {
    return `Schema '${schemaId}' not registered`;
  }
  const errs = bookstoreEntities.validate(schema as Record<string, unknown> & { '$id': string }, data);

  return errs.ok ? null : `Validation failed with ${errs.items.length} errors`;
}

console.assert(
  validateIfPresent(CustomerSchema.$id, aboxFixtures.customer) === null,
  'Valid customer should pass'
);

console.log('has CustomerSchema:', bookstoreEntities.registry.has(CustomerSchema.$id));
console.log('has urn:bookstore:NonExistent:', bookstoreEntities.registry.has('urn:bookstore:NonExistent'));
console.log('validateIfPresent(Customer, valid fixture):', validateIfPresent(CustomerSchema.$id, aboxFixtures.customer));
Output
Press Execute to run this example against the real library.

jt.registry.get(iri)

Retrieves the original schema object by $id. Returns Record<string, unknown> | undefined.

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

// BookSchema is a Compose.subClassOf composition: the registry stores the allOf
// form, so retrieving it gives back { $id, allOf } rather than a flat properties
// map. Flat schemas like BibliographicRecordSchema round-trip cleanly.
const bibliographic = bookstoreEntities.registry.get(BibliographicRecordSchema.$id);
const book = bookstoreEntities.registry.get(BookSchema.$id);

console.assert(bibliographic !== undefined, 'BibliographicRecordSchema should be retrievable');
console.assert(book !== undefined, 'BookSchema should be retrievable');
console.assert(
  (bibliographic?.properties as Record<string, unknown> | undefined)?.isbn !== undefined,
  'BibliographicRecordSchema.properties.isbn should exist'
);

if (bibliographic) {
  // Compose.pick on a flat schema — picks from its own properties.
  const BibliographicSummary = Compose.pick(
    bibliographic as typeof BibliographicRecordSchema,
    [
      'isbn',
      'title'
    ] as const,
    'https://bookstore.example/BibliographicSummary'
  );

  console.assert(typeof BibliographicSummary.$id === 'string', 'Composed schema should have $id');
  console.log('retrieved bibliographic $id:', bibliographic.$id);
  console.log('BibliographicSummary $id:', BibliographicSummary.$id);
  console.log('BibliographicSummary properties:', Object.keys(BibliographicSummary.properties));
}

// Composed schemas are stored as-is: Book's registry entry has allOf, not properties.
console.log('retrieved Book $id:', book?.$id);
console.log('Book registry shape keys:', book ? Object.keys(book) : '—');
Output
Press Execute to run this example against the real library.

jt.registry.keys() / values() / entries()

Standard Map iterators. keys() yields $id strings, values() yields schema objects, entries() yields [iri, schema] pairs.

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

const ids = [...bookstoreEntities.registry.keys()];
const bookstoreSchemas = ids.filter((id) => {
  return id.startsWith('urn:bookstore:');
});

console.assert(bookstoreSchemas.length > 0, 'Should have bookstore schemas');

for (const schema of bookstoreEntities.registry.values()) {
  // Each schema is always defined; just verify iteration works
  void schema;
}

for (const [
  iri,
  schema
] of bookstoreEntities.registry) {
  // iri and schema are always defined by the registry contract
  void iri;
  void schema;
}

const sizes: number[] = [];

for (const [
  _iri,
  _schema
] of bookstoreEntities.registry.entries()) {
  sizes.push(1);
}

console.assert(sizes.length === bookstoreEntities.registry.size, 'forEach count should match size');

console.log('total registered schemas:', bookstoreEntities.registry.size);
console.log('bookstore schemas:', bookstoreSchemas.length);
console.log('bookstore schema ids:', bookstoreSchemas);
Output
Press Execute to run this example against the real library.

jt.registry.set(schema, iri?)

Map-style write. Schema is always the first argument; key is derived from schema.$id. Pass an explicit iri only for non-canonical aliasing; passing one that disagrees with schema.$id throws SchemaError('SCHEMA_INVALID_INPUT'). Bulk writes accept an array of schemas or [schema, iri] tuples. Replaces silently on collision per Map.set. Returns the registry for chaining.

/**
 * Registry.set — fluent + bulk forms.
 *
 * `jt.set(schema)` adds one schema; `set([s1, s2, ...])`
 * adds a tuple. Both keep the canonical bookstore as the single source
 * of truth — derived schemas attach to it rather than building a
 * mini-registry.
 */

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

// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();

const PatchCustomerSchema = Compose.partial(
  CustomerSchema,
  'https://bookstore.example/RegistrySetPatch'
);
const SummaryCustomerSchema = Compose.pick(
  CustomerSchema,
  [
    'customerId',
    'name'
  ] as const,
  'https://bookstore.example/RegistrySetSummary'
);

jt.set(PatchCustomerSchema);
jt.set(SummaryCustomerSchema);

console.assert(jt.registry.has(PatchCustomerSchema.$id));
console.assert(jt.registry.has(SummaryCustomerSchema.$id));

console.log('PatchCustomer $id:', PatchCustomerSchema.$id);
console.log('SummaryCustomer $id:', SummaryCustomerSchema.$id);
console.log('both registered:', jt.registry.has(PatchCustomerSchema.$id) && jt.registry.has(SummaryCustomerSchema.$id));
Output
Press Execute to run this example against the real library.

jt.set(schema) is the type-accumulating wrapper that calls registry.set internally and widens the TypeScript type map. Use jt.set when you want the new schema's shape reflected in subsequent validate/instantiate/is calls; use jt.registry.set for hot-reload or test-fixture replacement where the static type doesn't need to follow.

jt.registry.delete(iri)

Returns true if a schema was removed, false if iri wasn't registered. Subsequent $ref resolution to the deleted IRI throws GraphError('REF_UNRESOLVED') on the next validate/instantiate call against any schema that points to it.

/**
 * registry.delete — Schema removal from the registry
 * Demonstrates: delete returns true on first call, false on second (idempotent)
 *
 * A temporary schema is registered and then deleted from the canonical
 * bookstore registry. The delete operation returns true when the schema
 * exists and false when it has already been removed.
 */

import { Compose } from '../../../src/index.js';
import {
  BibliographicRecordSchema,
  createBookstoreDocRegistry
} from '../bookstore/index.js';

// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();

// Register a temporary summary schema. isbn and title are bibliographic fields
// that live on BibliographicRecordSchema; Compose.pick reads own `properties`.
const BookSummarySchema = Compose.pick(
  BibliographicRecordSchema,
  [
    'isbn',
    'title'
  ] as const,
  'https://bookstore.example/BookSummaryTemp'
);

jt.set(BookSummarySchema);
console.assert(jt.registry.has(BookSummarySchema.$id));

// First delete returns true.
const first = jt.registry.delete(BookSummarySchema.$id);

console.assert(first);

// Second delete returns false — already removed.
const second = jt.registry.delete(BookSummarySchema.$id);

console.assert(!second);
console.assert(!jt.registry.has(BookSummarySchema.$id));

console.log('first delete (schema existed):', first);
console.log('second delete (already removed):', second);
console.log('schema present after deletion:', jt.registry.has(BookSummarySchema.$id));
Output
Press Execute to run this example against the real library.

jt.registry.clear()

Wipes every registered schema. Use in test teardown or when rebuilding the registry from scratch.

jt.registry.revision

Monotonically increasing counter bumped on every mutation (set, delete, clear). External code that caches derived views (ontology builders, compiled graphs) snapshots the revision and rebuilds when it advances. jt.ontology() uses this internally.

See also

Released under the MIT License.