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.
curie.expand(value); // Curie instance method, single required positional
Hash.value(input); // single required positional
Path.toAccess(jsonPointer); // single required positionalRequired positionals + options. The options bag is the final parameter and is always optional.
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.
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.
| Bag | Interface | Source |
|---|---|---|
QuadFactory.iri options | QuadFactoryIriOptsInterface | src/interfaces/QuadFactoryOpts.ts |
QuadFactory.literal options | QuadFactoryLiteralOptsInterface | src/interfaces/QuadFactoryOpts.ts |
QuadFactory.quad options | QuadFactoryQuadOptsInterface | src/interfaces/QuadFactoryOpts.ts |
QuadFactory.emitLiterals / emitConstraintLiteral options | QuadFactoryEmitOptsInterface | src/interfaces/QuadFactoryOpts.ts |
IdentifierIssuer constructor options | IdentifierIssuerOptsInterface | src/interfaces/IdentifierIssuerOpts.ts |
BaseError constructor options | BaseErrorOptionsType | src/types/ErrorOptions.ts |
SchemaError constructor options | SchemaErrorOptionsType | src/types/ErrorOptions.ts |
GraphError constructor options | GraphErrorOptionsType | src/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);
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');
Available static methods:
| Static | Instance 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.additionalPropertiesforextend,keysforpick/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);
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.
Related
instantiate- primary consumer ofSchemaRefvalidate- also accepts string or objectsubschemaAt- returns a composable sub-schemaSchemaRef
See also
- Picking a method - which method to call given your data source
- Bookstore domain - schemas used in examples