Skip to content

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/default conditional export paths on any json-tology subpath.
  • Loaders helpers use only globalThis.fetch and Promise, with no Node built-ins.
  • JsonTology.create is synchronous. Async fetching is isolated to JsonTology.prefetch.
  • Runtime dependencies: commander (CLI only, not pulled into browser bundles) and jsonld (used by ontology building). @rdfjs/types is types-only and carries zero runtime cost.

Released under the MIT License.