Skip to content

Argument conventions

Validation modes: Validation modes reference

The rule

Required arguments are positional. Optional values, overrides, and configuration form a single config object as the last parameter.

This convention applies uniformly to every public and internal callable in the package: instance methods, static facade methods, constructors, and low-level RDF helpers. Once you learn it for JsonTology, you know the shape of every other entry point.

Why

  • DX uniformity. Every signature reads the same way. You never need to remember which positional was which option.
  • Future-extensible. Adding a new optional knob means adding a key to the options interface, never another positional, never a breaking change.
  • Single trailing bag. No "two-options-objects" ambiguity, no boolean flags spread across positions.

Shapes

Required-only positionals. Methods with only required arguments take no trailing options bag.

ts
curie.expand(value);                       // Curie instance method, single required positional
Hash.value(input);                         // single required positional
Path.toAccess(jsonPointer);                // single required positional

Required positionals + options. The options bag is the final parameter and is always optional.

ts
jt.toQuads(schema, data, options?);
QuadFactory.iri(value, options?);                                // { curie? }
QuadFactory.literal(value, datatype, options?);                  // { curie? }
QuadFactory.quad(subject, predicate, object, options?);          // { curie?, graph? }
QuadFactory.emitLiterals(subject, predicate, relations, quads, options?);
QuadFactory.emitConstraintLiteral(subject, predicate, datatype, relations, quads, options?);

Constructors. Same rule: required positionals first, options bag last.

ts
new IdentifierIssuer(options?);                    // { prefix?, counter?, existingMap? }
new SchemaError(code, message, options?);          // { schemaId?, cause? }
new GraphError(code, message, options?);           // { pointer?, cause? }
new BaseError(code, message, options?);            // { retryable?, cause? }

Option interface names

Each options bag has a canonical interface declared in src/interfaces/. They are exported through json-tology/interfaces (type-only) so external callers can reference the exact shape they pass.

BagInterfaceSource
QuadFactory.iri optionsQuadFactoryIriOptsInterfacesrc/interfaces/QuadFactoryOpts.ts
QuadFactory.literal optionsQuadFactoryLiteralOptsInterfacesrc/interfaces/QuadFactoryOpts.ts
QuadFactory.quad optionsQuadFactoryQuadOptsInterfacesrc/interfaces/QuadFactoryOpts.ts
QuadFactory.emitLiterals / emitConstraintLiteral optionsQuadFactoryEmitOptsInterfacesrc/interfaces/QuadFactoryOpts.ts
IdentifierIssuer constructor optionsIdentifierIssuerOptsInterfacesrc/interfaces/IdentifierIssuerOpts.ts
BaseError constructor optionsBaseErrorOptionsTypesrc/types/ErrorOptions.ts
SchemaError constructor optionsSchemaErrorOptionsTypesrc/types/ErrorOptions.ts
GraphError constructor optionsGraphErrorOptionsTypesrc/types/ErrorOptions.ts

Universal SchemaRef

Every method that accepts a schema reference accepts both a string ID and a schema object:

/**
 * Argument conventions: universal SchemaRef
 *
 * Every method that accepts a schema reference accepts both a string `$id`
 * and a schema object. The runtime treats them identically — string IDs
 * are looked up in the registry, schema objects are registered (idempotent)
 * then run against.
 *
 * This example calls `instantiate` and `validate` both ways to show that
 * results are identical.
 */

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

// By string $id — registry lookup.
const customerById: Customer = bookstoreEntities.instantiate(
  CustomerSchema.$id,
  aboxFixtures.customer
);

// By schema object — idempotent registration then run.
const customerByObj = bookstoreEntities.instantiate(
  CustomerSchema,
  aboxFixtures.customer
);

console.assert(customerById.customerId === customerByObj.customerId);
console.assert(customerById.name === customerByObj.name);
console.log('instantiate by $id and by object agree → customerId:', customerById.customerId);

// Validate also accepts both forms.
const errsByStr = bookstoreEntities.validate(CustomerSchema.$id, aboxFixtures.customer);
const errsByObj = bookstoreEntities.validate(CustomerSchema, aboxFixtures.customer);

console.assert(errsByStr.length === 0);
console.assert(errsByObj.length === 0);
console.log('validate by $id errors:', errsByStr.length, '| by object errors:', errsByObj.length);
Output
Press Execute to run this example against the real library.

Resolution rule: if a string, look up in the registry; if an object with $id, register it (idempotent) then run against it.

Static counterparts Compile-time + Runtime

The static facade methods (JsonTology.dump, JsonTology.fromQuads, JsonTology.instantiate, JsonTology.materialize) are generic over the supplied schema and return the inferred type; no cast to the schema's inferred type is needed.

Every instance method has a static counterpart on JsonTology that creates an ephemeral registry, registers the schema, runs the operation, and returns. No shared state. No setup required.

/**
 * Argument conventions: static counterparts on JsonTology
 *
 * Every instance method has a static counterpart on `JsonTology`. Static
 * methods build a one-shot ephemeral registry containing only the supplied
 * schema, run the operation, and discard the registry. No shared state.
 * No setup required.
 *
 * Use static when you have a self-contained schema with no cross-schema
 * `$ref`. Use instance (bookstoreEntities) when schemas reference each
 * other or when you need invariants and computeds.
 */

import { JsonTology } from '../../../src/index.js';
import {
  aboxFixtures, bookstoreEntities, CustomerSchema, IsbnSchema, OrderSchema
} from '../bookstore/index.js';

// Instance form — reuses the compiled validator from the shared registry.
// CustomerSchema references Address, Email, etc.; those are all registered
// in bookstoreEntities so $refs resolve correctly.
const errs = bookstoreEntities.validate(CustomerSchema.$id, aboxFixtures.customer);

console.assert(errs.length === 0);
console.log('instance validate → errors:', errs.length);

// Static (one-shot) form — builds an ephemeral registry, registers the
// supplied schema, runs the operation, then discards the registry.
// Only works for self-contained schemas with no cross-schema $ref.
// IsbnSchema is a plain string primitive with no external $ref.
const isbnErrs = JsonTology.validate(IsbnSchema, aboxFixtures.rareBook.isbn);

console.assert(isbnErrs.length === 0);
console.log('static validate isbn →', aboxFixtures.rareBook.isbn, '| errors:', isbnErrs.length);

// One-shot instantiate — same pattern: self-contained schema only.
const isbn = JsonTology.instantiate(IsbnSchema, aboxFixtures.rareBook.isbn);

console.assert(isbn === aboxFixtures.rareBook.isbn);
console.log('static instantiate isbn →', isbn);

// One-shot toTbox — ontology from multiple schemas, no registry.
// The returned builder object is always defined; calling .jsonLd() on it
// serializes the TBox.
const tbox = JsonTology.toTbox([
  CustomerSchema,
  OrderSchema
]);

const tboxJson = tbox.jsonLd();

console.log('static toTbox serialized length:', tboxJson.length, 'chars');
Output
Press Execute to run this example against the real library.

Available static methods:

StaticInstance equivalent
JsonTology.is(schema, data)jt.is(schema, data)
JsonTology.validate(schema, data)jt.validate(schema, data)
JsonTology.instantiate(schema, data, options?)jt.instantiate(schema, data, options?)
JsonTology.materialize(schema, data?, options?)jt.materialize(schema, data?, options?)
JsonTology.subschemaAt(schema, pointer)jt.subschemaAt(schema, pointer)
JsonTology.dump(schema, value, options?)jt.dump(schema, value, options?)
JsonTology.dumpJson(schema, value, options?)jt.dumpJson(schema, value, options?)
JsonTology.toQuads(schema, data)jt.toQuads(schema, data)
JsonTology.fromQuads(schema, quads)jt.fromQuads(schema, quads)
JsonTology.toSchema(schema)jt.toSchema(schema)
JsonTology.toTbox(schemas)jt.toTbox()
JsonTology.toShacl(schemas)jt.toShacl()
JsonTology.ontology(schemas)jt.ontology()

Static methods create a fresh ephemeral instance per call. Use instance methods when you have multiple schemas that reference each other, or when you need to register invariants and computeds.

Compose argument order

The Compose.* helpers mint new schemas from existing ones. All positional arguments are required; there is no optional trailing options bag.

  • One source, minting a new ID: (source, <required middle arg>, newId), where the middle argument is the operation-specific required input (e.g. additionalProperties for extend, keys for pick/omit). Example: Compose.extend(UserSchema, additions, 'NewId')
  • Many sources: (sources, newId), exactly two arguments, no extras. Example: Compose.intersection([A, B] as const, 'NewId')

subschemaAt - composable pointer resolution

subschemaAt resolves a JSON Pointer within a parent schema and returns the sub-schema as a registerable schema object. The result can be passed directly to any of the four core methods:

/**
 * Argument conventions: subschemaAt — composable pointer resolution
 *
 * `subschemaAt` resolves a JSON Pointer within a parent schema and returns
 * the sub-schema as a registerable schema object with a synthesized `$id`
 * of the form `<parent.$id>#<pointer>`. The result can be passed directly
 * to any of the four core methods.
 *
 * This example resolves the `orderLines` array item sub-schema of `OrderSchema`
 * and validates a single order line against it.
 */

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

// Resolve the array item sub-schema at /properties/orderLines/items.
const itemSchema = bookstoreEntities.subschemaAt(
  OrderSchema.$id,
  '/properties/orderLines/items'
);

// The returned schema has a synthesized $id.
console.assert(typeof itemSchema.$id === 'string');
console.assert(itemSchema.$id.startsWith(OrderSchema.$id));
console.log('subschema $id:', itemSchema.$id);

// Validate a concrete order line item against the resolved sub-schema.
const line = aboxFixtures.order.orderLines[0];
const errs = bookstoreEntities.validate(itemSchema, line);

console.assert(errs.length === 0);
console.log('order line validate errors:', errs.length);

const bookIsbn: string = line.bookIsbn;

console.assert(bookIsbn === '9783522128001');
console.log('order line bookIsbn:', bookIsbn);
Output
Press Execute to run this example against the real library.

The returned schema has a synthesized $id of the form <parent.$id>#<pointer> and is automatically registered in the calling registry so subsequent operations work directly.

See also

Released under the MIT License.