Skip to content

Public utility classes

A small set of utility classes is exported alongside JsonTology for advanced use - cases where you reach below the facade for graph, RDF, or hashing primitives. Each utility has one responsibility.

ClassModulePurpose
Curiesrc/modules/rdf/Curie.tsCompact / expand IRIs against a prefix map
Pathsrc/modules/data/Path.tsConvert JSON Pointers to JS access form
Resolversrc/modules/data/Resolver.tsMerge per-call options with a base options object
Hashsrc/modules/hash/Hash.tsDeterministic FNV-1a hash of any JSON-serializable value
Liftsrc/modules/rdf/Lift.tsRDF interop helpers (RDF/JS quad conversion, lifting)
IdentifierIssuersrc/modules/rdf/IdentifierIssuer.tsPer-call blank-node counter for projection isolation

The bookstore domain in Bookstore Domain supplies prefixes and IRIs in the snippets.


Curie

new Curie(prefixes) returns a CURIE handler. compact(iri) shrinks a full IRI; expand(curie) resolves a compact form back to a full IRI.

/**
 * Public utility classes — Curie, Path, Resolver, Hash
 *
 * Demonstrates each utility against canonical bookstore IRIs and
 * JSON pointer paths from the aboxFixtures.
 */

import {
  Curie, Hash, Path, Resolver
} from '../../../src/index.js';
import { bookstoreEntities } from '../bookstore/index.js';
import { aboxFixtures } from '../bookstore/aboxFixtures.js';

// ──────────────────────────────────────────────────────────────────────────
// Curie: compact/expand IRIs against a prefix map

const curie = new Curie({
  'bookstore': 'https://bookstore.example/',
  'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
  'xsd': 'http://www.w3.org/2001/XMLSchema#'
});

// Expand compact CURIE form to full IRI
const expandedCustomer = curie.expand('bookstore:Customer');

console.assert(expandedCustomer === 'https://bookstore.example/Customer');

const expandedString = curie.expand('xsd:string');

console.assert(expandedString === 'http://www.w3.org/2001/XMLSchema#string');

// Compact full IRI to CURIE form (longest match)
const compactCustomer = curie.compact('https://bookstore.example/Customer');

console.assert(compactCustomer === 'bookstore:Customer');

const compactType = curie.compact('http://www.w3.org/1999/02/22-rdf-syntax-ns#type');

console.assert(compactType === 'rdf:type');

// ──────────────────────────────────────────────────────────────────────────
// Path: convert JSON Pointer to JS access form

const itemQuantityPointer = '/items/0/quantity';
const itemQuantityAccess = Path.toAccess(itemQuantityPointer);

console.assert(itemQuantityAccess === 'items[0].quantity');

const customerNamePointer = '/customer/name';
const customerNameAccess = Path.toAccess(customerNamePointer);

console.assert(customerNameAccess === 'customer.name');

const oddKeyPointer = '/oddly-shaped-key';
const oddKeyAccess = Path.toAccess(oddKeyPointer);

console.assert(oddKeyAccess === '["oddly-shaped-key"]');

const emptyPointer = '';
const emptyAccess = Path.toAccess(emptyPointer);

console.assert(emptyAccess === '');

// ──────────────────────────────────────────────────────────────────────────
// Resolver: merge per-call options with base options

const baseOptions = {
  'enableDefaults': true,
  'enableValidation': true
};
const overrideOptions = { 'enableDefaults': false };
const mergedOptions = Resolver.merge(baseOptions, overrideOptions);

console.assert(!mergedOptions.enableDefaults);
console.assert(mergedOptions.enableValidation);

const undefinedOverride: Partial<typeof baseOptions> = {};
const mergedWithUndefined = Resolver.merge(baseOptions, undefinedOverride);

console.assert(mergedWithUndefined.enableDefaults);

// ──────────────────────────────────────────────────────────────────────────
// Hash: deterministic FNV-1a hash of JSON-serializable values

const bookHash1 = Hash.value({
  'isbn': '9783522128001',
  'title': 'Die unendliche Geschichte'
});

// Key order does not matter — same hash
const bookHash2 = Hash.value({
  'isbn': '9783522128001',
  'title': 'Die unendliche Geschichte'
});

console.assert(bookHash1 === bookHash2);
console.assert(typeof bookHash1 === 'string');

// Different content produces different hashes
const differentHash = Hash.value({
  'isbn': '9780140449136',
  'title': 'War and Peace'
});

console.assert(bookHash1 !== differentHash);

// ──────────────────────────────────────────────────────────────────────────
// Integration: all utilities work with canonical bookstore entities

// Curie can expand bookstore IRIs using the registry's built-in prefix map
const ctx = bookstoreEntities.ontology().context();

console.assert(typeof ctx === 'object');

void aboxFixtures;

// Output demonstrating each utility's result
console.log('Curie.expand("bookstore:Customer"):', expandedCustomer);
console.log('Curie.compact("https://bookstore.example/Customer"):', compactCustomer);
console.log('Path.toAccess("/items/0/quantity"):', itemQuantityAccess);
console.log('Path.toAccess("/customer/name"):', customerNameAccess);
console.log('Resolver.merge result — enableDefaults:', mergedOptions.enableDefaults, '| enableValidation:', mergedOptions.enableValidation);
console.log('Hash.value (same objects equal):', bookHash1 === bookHash2);
console.log('Hash.value sample:', bookHash1);
Output
Press Execute to run this example against the real library.

jt.toCurie / jt.fromCurie

The JsonTology facade exposes two convenience methods that use the registry's merged prefix map (standard prefixes + any custom prefixes passed to create()):

  • jt.toCurie(iri) — compact a full IRI to CURIE form. Returns the input unchanged when no prefix matches.
  • jt.fromCurie(value) — expand a CURIE to its full IRI. Returns the input unchanged for non-CURIE strings and unknown prefixes.
/**
 * Advanced Example 110 — toCurie / fromCurie round-trip
 *
 * `toCurie(iri)` compacts a full IRI to its CURIE form using the registry's
 * merged prefix map (STANDARD_PREFIXES + any custom prefixes). When no prefix
 * matches, the input is returned unchanged.
 *
 * `fromCurie(value)` is the inverse: it expands a CURIE such as `ex:Customer`
 * to its full IRI. Non-CURIE strings (no matching prefix, no colon, absolute
 * IRIs whose scheme is not a registered prefix) pass through unchanged.
 *
 * Both methods share the same prefix map built at construction time from
 * `STANDARD_PREFIXES` merged with any `prefixes` option passed to `create()`.
 */

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

// A small schema so the registry has something registered.
const AuthorSchema = {
  '$id': 'https://bookstore.example/Author',
  'properties': { 'name': { 'type': 'string' } },
  'required': ['name'],
  'type': 'object'
} as const;

const jt = JsonTology.create({
  'baseIri': 'https://bookstore.example',
  'enableStrictGraph': false,
  'prefixes': { 'bk': 'https://bookstore.example/' },
  'schemas': [AuthorSchema] as const
});

// ── toCurie — compact full IRIs ───────────────────────────────────────────

// Standard RDFS prefix is built-in via STANDARD_PREFIXES.
const rdfsLabel = `${STANDARD_PREFIXES.rdfs}label`;

console.assert(
  jt.toCurie(rdfsLabel) === 'rdfs:label',
  'toCurie: rdfs:label compacts correctly'
);
console.log('toCurie rdfs:label →', jt.toCurie(rdfsLabel));

// Custom bookstore prefix registered via create() options.
console.assert(
  jt.toCurie('https://bookstore.example/Author') === 'bk:Author',
  'toCurie: bk:Author compacts correctly'
);
console.log('toCurie bk:Author   →', jt.toCurie('https://bookstore.example/Author'));

// No matching prefix — input is returned unchanged.
const unknownIri = 'https://unknown.example.org/Thing';

console.assert(
  jt.toCurie(unknownIri) === unknownIri,
  'toCurie: unknown IRI passes through unchanged'
);
console.log('toCurie unknown     →', jt.toCurie(unknownIri));

// ── fromCurie — expand CURIEs ─────────────────────────────────────────────

// Standard prefix expansion.
console.assert(
  jt.fromCurie('rdf:type') === 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type',
  'fromCurie: rdf:type expands to full IRI'
);
console.log('fromCurie rdf:type  →', jt.fromCurie('rdf:type'));

// Custom prefix expansion.
console.assert(
  jt.fromCurie('bk:Author') === 'https://bookstore.example/Author',
  'fromCurie: bk:Author expands to full IRI'
);
console.log('fromCurie bk:Author →', jt.fromCurie('bk:Author'));

// Unknown prefix — input is returned unchanged.
console.assert(
  jt.fromCurie('unknown:Thing') === 'unknown:Thing',
  'fromCurie: unknown prefix passes through unchanged'
);
console.log('fromCurie unknown   →', jt.fromCurie('unknown:Thing'));

// ── round-trip ────────────────────────────────────────────────────────────

const authorIri = 'https://bookstore.example/Author';
const authorCurie = jt.toCurie(authorIri);
const roundTripped = jt.fromCurie(authorCurie);

console.assert(
  roundTripped === authorIri,
  'round-trip: toCurie then fromCurie restores the original IRI'
);
console.log('\nRound-trip');
console.log('  IRI:        ', authorIri);
console.log('  toCurie:    ', authorCurie);
console.log('  fromCurie:  ', roundTripped);
console.log('  match:', roundTripped === authorIri);
Output
Press Execute to run this example against the real library.

When multiple prefixes share an overlap, compact picks the longest match.

The default prefix map used across the package is STANDARD_PREFIXES (from src/constants/STANDARD_PREFIXES.ts), the canonical prefix-to-namespace lookup for the well-known RDF vocabularies (rdf, rdfs, owl, sh, xsd, schema, foaf, dc, dct, dcterms, dcat, skos, prov, time, geo, vann, dash, jt). Every IRI constant in src/constants/IRI.ts derives from this map. Pass your own prefix map to JsonTology.create({ prefixes }) to extend or override the defaults; your entries merge over STANDARD_PREFIXES.

Path

Path.toAccess(jsonPointer) converts a JSON Pointer into JS access form - the path you would write to read the value out of the object. Useful when surfacing validation errors in UIs that expect access notation.

/**
 * Path: convert JSON Pointer to JS access notation
 *
 * Path.toAccess converts RFC 6901 JSON Pointers to JS dot/bracket notation
 * for use in UI error display and form libraries.
 */

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

const tests: Array<[string, string]> = [
  [
    '/items/0/quantity',
    'items[0].quantity'
  ],
  [
    '/customer/name',
    'customer.name'
  ],
  [
    '/oddly-shaped-key',
    '["oddly-shaped-key"]'
  ],
  [
    '',
    ''
  ]
];

for (const [
  pointer,
  expected
] of tests) {
  const result = Path.toAccess(pointer);

  console.assert(result === expected, `${pointer} -> ${result}`);
  console.log(`Path.toAccess(${JSON.stringify(pointer)}) => ${JSON.stringify(result)}`);
}
Output
Press Execute to run this example against the real library.

Numeric segments become [N]; identifier-shaped segments become .name; non-identifier segments are quoted with bracket notation.

Resolver

Resolver.merge(base, override) returns a fresh object with override's defined keys overwriting base. undefined keys in override do not erase base values - this is the per-call option-merge pattern used throughout json-tology.

/**
 * Resolver: merge per-call options without mutating base
 *
 * Resolver.merge implements safe per-call option merging where undefined
 * values in the override do not erase base values.
 */

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

const base = {
  'enableDefaults': true,
  'enableValidation': true
};
const merged = Resolver.merge(base, { 'enableDefaults': false });

console.assert(!merged.enableDefaults, 'override takes precedence');
console.assert(merged.enableValidation, 'base value preserved');

const sameAsBase = Resolver.merge(base, {});

console.assert(sameAsBase.enableDefaults, 'undefined does not erase');

console.log('Resolver.merge — override wins:', merged.enableDefaults, '(was true, overridden to false)');
console.log('Resolver.merge — base preserved:', merged.enableValidation);
console.log('Resolver.merge — undefined does not erase:', sameAsBase.enableDefaults);
Output
Press Execute to run this example against the real library.

Hash

Hash.value(input) returns a hex FNV-1a hash. Object keys are sorted before serialization, so two objects that differ only in key order produce the same hash.

/**
 * Hash: deterministic FNV-1a hash of JSON values
 *
 * Hash.value returns a hex FNV-1a hash. Object keys are sorted before
 * serialization, so key order does not matter.
 */

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

const schemaA = {
  'isbn': '9780140449136',
  'title': 'War and Peace'
};
const schemaB = {
  'isbn': '9780140449136',
  'title': 'War and Peace'
};

const hashA = Hash.value(schemaA);
const hashB = Hash.value(schemaB);

console.assert(hashA === hashB, 'key order does not matter');
console.assert(typeof hashA === 'string', 'hash is string');

// Use as a cache key
const cache = new Map<string, unknown>();
const key = Hash.value(schemaA);

if (!cache.has(key)) {
  cache.set(key, { 'computed': true });
}
console.assert(cache.has(key), 'cache keyed by hash');

console.log('Hash.value (key-order-stable):', hashA === hashB);
console.log('Hash.value sample:', hashA);
Output
Press Execute to run this example against the real library.

Used internally by registerAnonymous to mint synthetic $id values from schema content. Use it directly when you need a stable cache key for a structured value.

Lift

The Lift module exposes interop helpers between RDF/JS quads (from libraries like n3 or eyereasoner) and json-tology's internal quad shape, plus the Lift.instances method that powers JsonTology.fromQuads.

/**
 * n3 RDF/JS quads consumed directly by fromQuads
 *
 * Parses Turtle for the canonical Bastian-rare-book IRI via the n3 library,
 * narrows the result to spec-compliant rdf/js quads via
 * `Lists.narrowExternalQuads`, and hands them to json-tology unchanged.
 * Any rdf/js-emitting parser (n3, rdflib, etc.) can drive the registry
 * through the same path — terms with full IRIs in `.value` pass through.
 */

import { Parser } from 'n3';
import { Lists } from '../../../src/index.js';
import type { QuadInterface } from '../../../src/interfaces/QuadInterface.js';
import { aboxFixtures } from '../bookstore/index.js';

const isbn = aboxFixtures.rareBook.isbn;
const turtle = `
  <urn:bookstore:rarebook:neverending-1979-thienemann>
    a <urn:bookstore:Book> ;
    <https://bookstore.example/isbn> "${isbn}" ;
    <https://bookstore.example/title> "Die unendliche Geschichte" .
`;

const parser = new Parser();
const rdfQuads = parser.parse(turtle);
const internal: QuadInterface[] = Lists.narrowExternalQuads(rdfQuads);

console.assert(internal.length > 0);
console.assert(typeof internal[0]?.subject.value === 'string');

console.log('Lists.narrowExternalQuads — quad count:', internal.length);
console.log('First quad subject:', internal[0]?.subject.value);
console.log('First quad predicate:', internal[0]?.predicate.value);
Output
Press Execute to run this example against the real library.

For the typed round-trip use the JsonTology facade (RDF round-trip). Reach for Lift only when integrating with an external RDF/JS library directly.

IdentifierIssuer

Ported from the W3C RDF Dataset Canonicalization algorithm. Each projector call (Projection.graph, Projection.abox, OwlProjection.graph, ShaclProjection.graph) constructs its own IdentifierIssuer so concurrent serializations never share mutable counter state.

Constructor: new IdentifierIssuer(options?) where options is { prefix?: string; counter?: number; existingMap?: ReadonlyMap<string, string> }. The default prefix is '_:b' (RDF blank-node syntax); counter starts at zero; existingMap seeds a prior issuance history (used by clone()).

Surface:

  • getId(existing?): issue a new identifier, or return the previously issued one for existing if it has been mapped. Calling without existing always issues a fresh identifier without recording a mapping (anonymous blank nodes).
  • hasId(existing): true if existing already has an issued identifier.
  • getIssuedMap(): read-only view of the current mapping.
  • getIssuedIdentifiers(): keys in issuance order.
  • clone(): fork an independent issuer with the same prefix, counter, and mappings.
  • reset(): clear counter and mappings.

You only need to construct one directly if you are writing a custom projector or RDF serializer that participates in the same blank-node naming scheme. The built-in projectors manage their own.


Examples

Example 1: Curie: compact bookstore IRIs for display

Shrink full IRIs to CURIE form for readable output in debug logs or ontology tooling.

/**
 * Curie — compact bookstore IRIs for display.
 *
 * Shrink full IRIs to CURIE form for readable output in debug logs or ontology
 * tooling. When multiple prefixes share an overlap, `compact` picks the longest
 * match. `expand` resolves a compact form back to the full IRI.
 *
 * Demonstrates: Curie.compact / expand with bookstore, owl, and xsd prefixes.
 */

import { Curie } from '../../../src/index.js';
import {
  BookSchema, CustomerSchema, OrderSchema
} from '../bookstore/index.js';

const curie = new Curie({
  'bk': 'https://bookstore.example/',
  'owl': 'http://www.w3.org/2002/07/owl#',
  'xsd': 'http://www.w3.org/2001/XMLSchema#'
});

// Use the registered bookstore baseIri prefix for compacting display names
const bookIri = `https://bookstore.example/${BookSchema.$id.replace('urn:bookstore:', '')}`;
const customerIri = `https://bookstore.example/${CustomerSchema.$id.replace('urn:bookstore:', '')}`;
const orderIri = `https://bookstore.example/${OrderSchema.$id.replace('urn:bookstore:', '')}`;

console.assert(
  curie.compact(bookIri) === 'bk:Book',
  'Book IRI compacts to bk:Book'
);
console.assert(
  curie.compact(customerIri) === 'bk:Customer',
  'Customer IRI compacts to bk:Customer'
);
console.assert(
  curie.compact(orderIri) === 'bk:Order',
  'Order IRI compacts to bk:Order'
);
console.assert(
  curie.compact('http://www.w3.org/2002/07/owl#Class') === 'owl:Class',
  'OWL Class IRI compacts to owl:Class'
);

// expand inverts compact
console.assert(
  curie.expand('bk:Customer') === 'https://bookstore.example/Customer',
  'bk:Customer expands to full IRI'
);

console.log('Curie.compact(Book IRI):', curie.compact(bookIri));
console.log('Curie.compact(Customer IRI):', curie.compact(customerIri));
console.log('Curie.compact(Order IRI):', curie.compact(orderIri));
console.log('Curie.compact(owl:Class):', curie.compact('http://www.w3.org/2002/07/owl#Class'));
console.log('Curie.expand("bk:Customer"):', curie.expand('bk:Customer'));
Output
Press Execute to run this example against the real library.

Example 2: Path: surface validation error paths in a form UI

Convert JSON Pointer paths from ValidationErrors into JS access notation for a form library that uses dot/bracket paths.

/**
 * Path: surface validation error paths in a form UI
 *
 * Convert JSON Pointer paths from ValidationErrors into JS access notation
 * for form libraries that use dot/bracket paths.
 */

import { Path } from '../../../src/index.js';
import {
  bookstoreEntities, OrderSchema
} from '../bookstore/index.js';

// quantity: 0 violates QuantitySchema minimum: 1, so the error path is /orderLines/0/quantity
const errs = bookstoreEntities.validate(OrderSchema.$id, {
  'customerId': 'c1a2b3d4-e5f6-7890-abcd-ef1234567890',
  'orderId': 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
  'orderLines': [{
    'bookIsbn': '9780140449136',
    'quantity': 0,
    'unitPrice': {
      'amount': 1499,
      'currency': 'EUR'
    }
  }],
  'orderTotal': {
    'amount': 50,
    'currency': 'EUR'
  },
  'placedAt': '2026-01-15T10:30:00Z',
  'shippingAddress': {
    'city': 'Berlin',
    'country': 'DE',
    'postalCode': '10115',
    'street': 'Unter den Linden 1'
  }
});

for (const err of errs) {
  const accessPath = Path.toAccess(err.path);

  console.assert(typeof accessPath === 'string', `error path converted: ${accessPath}`);
  console.log(`Validation error at ${err.path} => JS access: ${accessPath} (${err.keyword})`);
}
Output
Press Execute to run this example against the real library.

Example 3: Resolver: merge per-call options without mutating the base

Override a single flag for one call without constructing a full options object each time.

/**
 * Resolver: override a single flag for one call
 *
 * Resolver.merge allows per-call option overrides without mutating the base
 * options object or constructing a full options object each time.
 */

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

const defaultOpts = {
  'enableDefaults': true,
  'enableThrow': false,
  'enableValidation': true
};

// Per-call: turn off defaults for one strict parse
const strictOpts = Resolver.merge(defaultOpts, { 'enableDefaults': false });

console.assert(!strictOpts.enableDefaults, 'defaults disabled');
console.assert(strictOpts.enableValidation, 'validation enabled');
console.assert(!strictOpts.enableThrow, 'throw disabled');

// undefined does not erase base values — pass an empty partial to leave enableDefaults intact
const sameAsDefault = Resolver.merge(defaultOpts, {});

console.assert(sameAsDefault.enableDefaults, 'base value used when override is undefined');

console.log('strictOpts.enableDefaults:', strictOpts.enableDefaults, '| strictOpts.enableValidation:', strictOpts.enableValidation);
console.log('sameAsDefault.enableDefaults:', sameAsDefault.enableDefaults);
Output
Press Execute to run this example against the real library.

Example 4: Hash: stable cache key for a schema content fingerprint

Use Hash.value to produce a deterministic fingerprint for a schema object. Two structurally identical schemas with different key order produce the same hash.

/**
 * Hash: stable cache key for schema content fingerprint
 *
 * Use Hash.value to produce a deterministic fingerprint for a schema object.
 * Two structurally identical schemas with different key order produce the same hash.
 */

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

const schemaA = {
  'properties': {
    'isbn': { 'type': 'string' },
    'title': { 'type': 'string' }
  },
  'type': 'object'
};
const schemaB = {
  'properties': {
    'isbn': { 'type': 'string' },
    'title': { 'type': 'string' }
  },
  'type': 'object'
};

const hashA = Hash.value(schemaA);
const hashB = Hash.value(schemaB);

console.assert(hashA === hashB, 'key order does not matter');

// Use as a cache key
const cache = new Map<string, unknown>();
const key = Hash.value(schemaA);

if (!cache.has(key)) {
  cache.set(key, { 'computedResult': 'expensive computation' });
}
console.assert(cache.has(key), 'cache entry present');

console.log('Hash.value (structurally equal schemas):', hashA === hashB);
console.log('Cache key:', key);
console.log('Cache hit:', cache.has(key));
Output
Press Execute to run this example against the real library.

Example 5: Lift: integrate an external n3 RDF/JS source

Convert quads produced by the n3 parser into json-tology's internal quad shape for fromQuads.

/**
 * n3 RDF/JS quads consumed directly by fromQuads
 *
 * Parses Turtle for the canonical Bastian-rare-book IRI via the n3 library,
 * narrows the result to spec-compliant rdf/js quads via
 * `Lists.narrowExternalQuads`, and hands them to json-tology unchanged.
 * Any rdf/js-emitting parser (n3, rdflib, etc.) can drive the registry
 * through the same path — terms with full IRIs in `.value` pass through.
 */

import { Parser } from 'n3';
import { Lists } from '../../../src/index.js';
import type { QuadInterface } from '../../../src/interfaces/QuadInterface.js';
import { aboxFixtures } from '../bookstore/index.js';

const isbn = aboxFixtures.rareBook.isbn;
const turtle = `
  <urn:bookstore:rarebook:neverending-1979-thienemann>
    a <urn:bookstore:Book> ;
    <https://bookstore.example/isbn> "${isbn}" ;
    <https://bookstore.example/title> "Die unendliche Geschichte" .
`;

const parser = new Parser();
const rdfQuads = parser.parse(turtle);
const internal: QuadInterface[] = Lists.narrowExternalQuads(rdfQuads);

console.assert(internal.length > 0);
console.assert(typeof internal[0]?.subject.value === 'string');

console.log('Lists.narrowExternalQuads — quad count:', internal.length);
console.log('First quad subject:', internal[0]?.subject.value);
console.log('First quad predicate:', internal[0]?.predicate.value);
Output
Press Execute to run this example against the real library.

Bad examples: what NOT to do

Anti-pattern 1: Curie: expanding a prefix that is not registered

/**
 * Curie anti-pattern — expanding a prefix that is not registered.
 *
 * `expand` on an unknown prefix returns the input unchanged rather than
 * throwing. This silently produces a syntactically invalid IRI. Always
 * register every prefix you intend to expand.
 *
 * Demonstrates: unknown-prefix expand returns input unchanged (anti-pattern
 * vs. correct pattern).
 */

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

// Anti-pattern: only the bk prefix is registered
const curieMissingSchema = new Curie({ 'bk': 'https://bookstore.example/' });
const unexpanded = curieMissingSchema.expand('schema:Book');

// expand returns the input unchanged — not a valid IRI
console.assert(
  unexpanded === 'schema:Book',
  'unknown prefix: expand returns input unchanged (not a valid IRI)'
);

// Correct pattern: register all prefixes before expanding
const curieWithSchema = new Curie({
  'bk': 'https://bookstore.example/',
  'schema': 'https://schema.org/'
});

console.assert(
  curieWithSchema.expand('schema:Book') === 'https://schema.org/Book',
  'registered prefix: expand resolves to full IRI'
);

console.log('Unknown prefix — expand returns input unchanged:', unexpanded);
console.log('Registered prefix — expand resolves correctly:', curieWithSchema.expand('schema:Book'));
Output
Press Execute to run this example against the real library.

Anti-pattern 2: Path: using toAccess on a non-JSON Pointer string

/**
 * Path anti-pattern — passing a dot-path instead of a JSON Pointer.
 *
 * `Path.toAccess` expects an RFC 6901 JSON Pointer (leading slash,
 * slash-separated segments). Passing a JS dot-path (e.g. `items.0.quantity`)
 * treats the whole string as one segment and produces incorrect output.
 *
 * Demonstrates: wrong vs. correct input for Path.toAccess.
 */

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

// Anti-pattern: dot-path is not a JSON Pointer — no leading slash means
// split('/').slice(1) produces an empty array, so the result is '' (root pointer)
const wrongResult = Path.toAccess('items.0.quantity');

// Without a leading slash the string is treated as a root pointer — result is ''
console.assert(
  wrongResult === '',
  'dot-path without leading slash collapses to empty string (incorrect for navigation)'
);

// Correct: pass a valid RFC 6901 JSON Pointer with leading slash
const correctResult = Path.toAccess('/items/0/quantity');

console.assert(
  correctResult === 'items[0].quantity',
  'valid JSON Pointer converts to JS access notation'
);

console.log('Dot-path (anti-pattern) returns:', JSON.stringify(wrongResult), '— empty, not navigable');
console.log('JSON Pointer (correct) returns:', JSON.stringify(correctResult));
Output
Press Execute to run this example against the real library.

Anti-pattern 3: Hash: using Hash.value as a cryptographic hash

/**
 * Hash anti-pattern — using Hash.value for security-sensitive purposes.
 *
 * `Hash.value` uses FNV-1a, a non-cryptographic hash. It is not suitable for
 * tokens, signatures, or deduplication of untrusted input. For security use
 * cases, reach for a cryptographic hash (Web Crypto API in browsers,
 * `node:crypto` in Node) — not `Hash.value`.
 *
 * This example demonstrates Hash.value's characteristics (deterministic,
 * key-order-stable, fast) and explains why these properties are insufficient
 * for security contexts. No node:crypto import is needed to make that point.
 *
 * Demonstrates: Hash.value is deterministic and key-order-stable but not
 * collision-resistant; the antipattern is reaching for it in security contexts.
 */

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

// A payload representative of a bookstore session token (never use Hash for this)
const sessionPayload = {
  'customerId': 'c1a2b3d4-e5f6-7890-abcd-ef1234567890',
  'exp': 1_735_689_600
};

// Anti-pattern: FNV-1a is NOT cryptographically secure.
// Do not use Hash.value for tokens, signatures, or security-sensitive dedup.
const fnvHash = Hash.value(sessionPayload);

console.assert(typeof fnvHash === 'string', 'Hash.value returns a string');
console.assert(fnvHash.length > 0, 'Hash.value is non-empty');
console.log('Hash.value (FNV-1a, NOT for security):', fnvHash);

// Hash.value is deterministic: same input always produces same output.
const repeated = Hash.value(sessionPayload);

console.assert(fnvHash === repeated, 'Hash.value is deterministic');
console.log('Deterministic (same input -> same hash):', fnvHash === repeated);

// Hash.value is key-order-stable: object key order does not affect the result.
// Both orderings of the same keys produce the identical hash.
const payloadAlt = Object.fromEntries(Object.entries(sessionPayload).reverse()) as typeof sessionPayload;
const reordered = Hash.value(payloadAlt);

console.assert(fnvHash === reordered, 'Hash.value is key-order-stable');
console.log('Key-order-stable (reordered keys -> same hash):', fnvHash === reordered);

// The antipattern: FNV-1a produces short hashes vulnerable to collision attacks.
// A 32-bit FNV-1a hash has 2^32 possible values — trivially brute-forced.
// For security: use Web Crypto (browser) or node:crypto (Node).
//
//   // Browser / Node 18+
//   const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(payload)));
//   const safeHash = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2,'0')).join('');
//
// SHA-256 produces 256-bit output — 2^256 possible values, not feasibly bruted.
// Use Hash.value for cache keys and schema fingerprints; use Web Crypto for tokens.

console.log('FNV-1a hash length (bits, approx):', fnvHash.length * 4);
console.log('SHA-256 hex length would be: 64 (256 bits) — far stronger for security');
Output
Press Execute to run this example against the real library.

Anti-pattern 4: Lift: passing Lift quads directly to a native RDF/JS consumer

/**
 * json-tology quads are rdf/js-compatible — no conversion bridge needed.
 *
 * `QuadInterface` is structurally compatible with @rdfjs/types: subject,
 * predicate, graph, and object are all term objects (termType + value + equals)
 * carrying full IRIs in `.value`. Internal quads from `toQuads()` flow into any
 * rdf/js consumer; external rdf/js quads (from n3, rdflib, eyereasoner) flow
 * back through `fromQuads` directly. `Lists.narrowExternalQuads` filters and
 * type-narrows external quads when needed.
 */

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

const internalQuads = bookstoreEntities.toQuads(CustomerSchema, aboxFixtures.customer);

console.assert(internalQuads.length > 0, 'quads produced');

const first = internalQuads[0];

if (first === undefined) {
  throw new Error('expected quads');
}

console.assert(
  typeof first.subject.value === 'string',
  'quad subject is an rdf/js term object with a .value string'
);
console.assert(
  first.subject.termType === 'NamedNode',
  'toQuads produces NamedNode subjects for hash-minted IRIs'
);

const lifted = bookstoreEntities.fromQuads(CustomerSchema.$id, internalQuads);

console.assert(lifted.length > 0, 'fromQuads recovers typed objects from quads');

console.assert(
  first.predicate.value.startsWith('http') || first.predicate.value.startsWith('urn'),
  'predicate.value is a full IRI, not a compact CURIE'
);

console.log('toQuads — quad count:', internalQuads.length);
console.log('First quad subject termType:', first.subject.termType);
console.log('First quad predicate (full IRI):', first.predicate.value);
console.log('fromQuads recovered object count:', lifted.length);
Output
Press Execute to run this example against the real library.

Comparison

ts
import { Curie, Path, Resolver, Hash, Lift } from 'json-tology';
// Five focused single-responsibility utilities;
// Curie and Lift are RDF-aware; Hash is key-order-stable;
// Path bridges JSON Pointer ↔ JS access notation;
// Resolver implements safe per-call option merge.
ts
// Zod provides no IRI/CURIE utilities, no JSON Pointer path conversion,
// no deterministic hashing, and no RDF interop.
// These concerns are out of scope for a schema validation library.
ts
// Same as Zod — no IRI, path-conversion, hashing, or RDF utilities.
ts
// AJV exposes no path-conversion or hashing utilities.
// JSON Pointer paths appear in ajv.errors[].instancePath as raw strings;
// conversion to JS access notation requires a third-party library (e.g. json-pointer).
ts
// rdflib and n3 provide CURIE-like prefix expansion via NamedNode / DataFactory,
// but no JSON Pointer path conversion, no schema-aware hashing, and no typed
// instance pipeline. They operate at the quad level only.
import { DataFactory } from 'n3';
const { namedNode } = DataFactory;
const bookIri = namedNode('https://bookstore.example/Book');
// Limitation: no compact/expand API; no option-merge; no FNV hash.
ts
// fast-json-patch applies JSON Patch operations and parses JSON Pointer paths,
// but does not convert pointers to JS access notation, hash structured values,
// or handle IRI/CURIE expansion.
import { getValueByPointer } from 'fast-json-patch';
const val = getValueByPointer(obj, '/items/0/quantity');
// Limitation: no toAccess conversion; no CURIE; no Hash; no RDF interop.

See also

Released under the MIT License.