Browser usage
json-tology has no environment-specific export paths. The same import works in Node, Bun, Deno, and browsers, with no conditional browser/node exports to navigate. Async schema fetching runs through the loader hook consumed by JsonTology.prefetch, which uses globalThis.fetch and therefore runs identically everywhere.
The shape is two steps: prefetch the snapshot once (async), then construct the instance synchronously anywhere from the snapshot.
CDN (no bundler)
html
<script type="module">
import { JsonTology, Loaders } from 'https://esm.sh/json-tology';
const snapshot = await JsonTology.prefetch({
loader: Loaders.fetch({ base: 'https://schemas.myapp.io/v1/' }),
rootIds: ['https://schemas.myapp.io/v1/User'],
});
const jt = JsonTology.create({
baseIri: 'https://myapp.io',
prefetched: snapshot,
});
const result = jt.validate('https://schemas.myapp.io/v1/User', formData);
</script>Bundler (Vite, esbuild, webpack)
/**
* Bundler pattern — pre-bundle local schemas with network fallback.
*
* In a bundler context (Vite, esbuild, webpack), local schemas are imported
* at build time and seeded into `Loaders.memory`. A `Loaders.compose` chain
* tries memory first; for any IRI not in the bundle the chain falls back to
* the next loader (here a stub that returns null so the example runs
* deterministically offline — in production this slot carries
* `Loaders.fetch({ base: '…' })`).
*
* Demonstrates: Loaders.compose + Loaders.memory seeded from the canonical
* bookstore schema set + prefetch + synchronous create.
*/
import {
JsonTology,
Loaders
} from '../../../src/index.js';
import {
bookstoreSchemas,
CustomerSchema,
OrderSchema
} from '../bookstore/index.js';
// Network-fallback stub — in production replace with
// `Loaders.fetch({ base: 'https://schemas.example/v1/' })`. Returning null
// lets `Loaders.compose` move on to the next loader (none here) and keeps
// the example runnable in offline test environments.
const offlineFallback = (): Promise<null> => {
return Promise.resolve(null);
};
const snapshot = await JsonTology.prefetch({
'loader': Loaders.compose(
// Pre-bundled schemas — memory fast path, no network for known IRIs.
Loaders.memory(new Map(bookstoreSchemas.map((schema) => {
return [
schema.$id,
schema
] as const;
}))),
offlineFallback
),
'schemas': [
CustomerSchema,
OrderSchema
]
});
const jt = JsonTology.create({
'baseIri': 'https://bookstore.example',
'prefetched': snapshot,
'schemas': [
CustomerSchema,
OrderSchema
] as const
});
// validate is synchronous — async work was isolated to prefetch
const result = jt.validate(CustomerSchema.$id, {
'addresses': [],
'customerId': 'c1a2b3d4-e5f6-7890-abcd-ef1234567890',
'email': 'bastian.bux@bookstore.example',
'name': 'Bastian Balthazar Bux'
});
console.assert(result.ok, 'Customer validates against prefetched registry');
console.log('Prefetch + sync create: snapshot schemas:', snapshot.schemas.size, '| validate ok:', result.ok);
Output
Press Execute to run this example against the real library.Node (same API)
/**
* Node pattern — prefetch with Loaders.cached wrapping Loaders.memory.
*
* The same `JsonTology.prefetch` + `create` pattern works identically in Node,
* Bun, Deno, and browsers. Wrapping the loader with `Loaders.cached` prevents
* re-fetching the same IRI if `prefetch` is called again (e.g., after a hot
* reload or a second call with an expanded schema list).
*
* The memory loader is seeded from the canonical `bookstoreSchemas` array so
* every transitive `$ref` (Email, Address, CityName, …) resolves locally.
*
* Demonstrates: Loaders.cached wrapping Loaders.memory; prefetch + synchronous
* create against the canonical bookstore.
*/
import {
JsonTology,
Loaders
} from '../../../src/index.js';
import {
bookstoreSchemas,
CustomerSchema
} from '../bookstore/index.js';
const memoryLoader = Loaders.memory(new Map(bookstoreSchemas.map((schema) => {
return [
schema.$id,
schema
] as const;
})));
const snapshot = await JsonTology.prefetch({
'loader': Loaders.cached(memoryLoader),
'schemas': [CustomerSchema]
});
const jt = JsonTology.create({
'baseIri': 'https://bookstore.example',
'prefetched': snapshot,
'schemas': [CustomerSchema] as const
});
// Synchronous validate after async prefetch
const valid = jt.validate(CustomerSchema.$id, {
'addresses': [],
'customerId': 'f1e2d3c4-b5a6-4789-8abc-def012345678',
'email': 'cornelia.funke@bookstore.example',
'name': 'Cornelia Funke'
});
console.assert(valid.ok, 'validate result is ok');
console.log('Node pattern — prefetch + cached loader + validate ok:', valid.ok);
console.log('Schemas in cached loader:', bookstoreSchemas.length);
Output
Press Execute to run this example against the real library.For local file loading, write a four-line fs loader:
/**
* Custom in-memory loader — resolve schemas from a pre-built map.
*
* Any function with signature `(iri: string) => Promise<JsonSchemaType | null>`
* is a valid loader. This example builds a small in-memory map of schema IRI
* to schema object and a loader function that resolves from it — no disk I/O,
* no Node built-ins, runs identically in browsers, workers, and Node.
*
* In a real setup the map would be populated from a bundled import or a prior
* fetch; here we use the bookstore schemas directly so the example is
* self-contained and verifiable.
*
* Demonstrates: custom loader function, `null` on miss, in-memory pattern.
*/
import {
CustomerSchema,
IsbnSchema
} from '../bookstore/index.js';
/** In-memory schema store keyed by schema $id. */
const schemaMap = new Map<string, Record<string, unknown>>([
[
CustomerSchema.$id,
CustomerSchema
],
[
IsbnSchema.$id,
IsbnSchema
]
]);
/**
* Loader: resolves a schema IRI from the in-memory map.
* Returns `null` for IRIs that have no registered entry.
*/
const memoryLoader = async (iri: string): Promise<null | Record<string, unknown>> => {
return schemaMap.get(iri) ?? null;
};
// Loader resolves a known schema IRI
const customerResult = await memoryLoader(CustomerSchema.$id);
console.assert(customerResult !== null, 'known schema IRI resolves via memory loader');
console.assert(
customerResult !== null && customerResult.$id === CustomerSchema.$id,
'resolved schema carries correct $id'
);
console.log('customerResult.$id:', customerResult?.$id);
// IsbnSchema also resolves
const isbnResult = await memoryLoader(IsbnSchema.$id);
console.assert(isbnResult !== null, 'Isbn schema resolves from map');
console.log('isbnResult.$id:', isbnResult?.$id);
// Unknown IRI returns null without throwing
const unknown = await memoryLoader('urn:bookstore:NoSuchThing');
console.assert(unknown === null, 'unknown IRI returns null');
console.log('unknown IRI returns null:', unknown === null);
Output
Press Execute to run this example against the real library.Schema-only (no $ref federation)
If all schemas are known at build time and have no external $refs, skip prefetch entirely:
/**
* Schema-only — skip prefetch when all schemas are local.
*
* If all schemas are known at build time and have no external `$ref`s that
* point outside the registered set, `JsonTology.prefetch` can be omitted
* entirely. Pass the full transitive closure of schemas directly to
* `JsonTology.create`.
*
* This is the simplest path for applications whose schemas are fully
* self-contained or pre-bundled.
*
* Demonstrates: JsonTology.create without prefetch, using the canonical
* bookstore registry.
*/
import { bookstoreEntities } from '../bookstore/index.js';
import { CustomerSchema } from '../bookstore/index.js';
// All schemas live in the canonical bookstore registry — every $ref
// (Address, Email, CityName, CountryCode, …) resolves locally without
// any loader.
const result = bookstoreEntities.validate(CustomerSchema.$id, {
'addresses': [],
'customerId': 'a1b2c3d4-e5f6-4890-abcd-ef1234567890',
'email': 'walter.moers@bookstore.example',
'name': 'Walter Moers'
});
console.assert(result.ok, 'validate returns ok result');
console.log('Schema-only (no prefetch) — validate ok:', result.ok);
console.log('ValidationErrors count:', result.items.length);
Output
Press Execute to run this example against the real library.Key points
- No
browser/node/defaultconditional export paths on any json-tology subpath. Loadershelpers use onlyglobalThis.fetchandPromise, with no Node built-ins.JsonTology.createis synchronous. Async fetching is isolated toJsonTology.prefetch.- Runtime dependencies:
commander(CLI only, not pulled into browser bundles) andjsonld(used by ontology building).@rdfjs/typesis types-only and carries zero runtime cost.