Getting Started
json-tology is an ontology-native type system for TypeScript. Declare schemas once in JSON Schema, get TypeScript types, runtime validation, defaults, transforms, and serialization from one canonical graph.
Install
npm install json-tologyDefine a schema
Schemas are plain JSON Schema objects with $id and as const. The bookstore domain used in all examples follows the one-file-per-concept pattern. See the Bookstore Domain page for the full folder layout and all schemas.
Primitives are named, reusable schemas with a urn: IRI:
// entities/CustomerId.ts
export const CustomerIdSchema = {
$id: 'urn:bookstore:CustomerId',
type: 'string',
format: 'uuid',
} as const;Entities compose primitives via $ref: SourceSchema.$id - never bare string literals:
// entities/Customer.ts
import { CustomerIdSchema } from './CustomerId.js';
import { EmailSchema } from './Email.js';
import { PersonNameSchema } from './PersonName.js';
const CustomerSchema = {
$id: 'urn:bookstore:Customer',
type: 'object',
properties: {
id: { $ref: CustomerIdSchema.$id },
email: { $ref: EmailSchema.$id },
name: { $ref: PersonNameSchema.$id },
},
required: ['id', 'email', 'name'],
} as const;as const is required. Without it TypeScript widens every string literal and InferType<T> cannot produce the right type.
Derive the TypeScript type
import type { InferType } from 'json-tology/types';
type Customer = InferType<typeof CustomerSchema>;
// {
// readonly id: string & FormatBrand<'uuid'>;
// readonly email: string & FormatBrand<'email'>;
// readonly name: string;
// }No code generation. No separate type declaration file. The type comes directly from the schema literal at compile time. See Type Inference for how $ref, enums, brands, and cross-schema references work.
Create an instance and register schemas
import { JsonTology } from 'json-tology';
const jt = JsonTology.create({
baseIRI: 'https://bookstore.example',
schemas: [CustomerSchema] as const,
});JsonTology.create() registers all schemas, compiles the validation graph, and builds the type map. Every method that accepts a schema $id returns typed results from that map.
Validate
validate() returns a ValidationErrors collection. An empty collection (errs.ok === true) means valid.
// Valid customer
const errs = jt.validate(CustomerSchema.$id, {
id: 'c1a2b3d4-e5f6-7890-abcd-ef1234567890',
email: 'alice@bookstore.example',
name: 'Alice Chen',
});
console.log(errs.ok); // true
// Missing required field
const bad = jt.validate(CustomerSchema.$id, { email: 'alice@bookstore.example' });
console.log(bad.length); // 2
for (const err of bad) {
console.log(err.path, err.keyword, err.message);
}See Validation for is(), validate(), subschemaAt(), and the structured error views.
Instantiate
instantiate() validates, applies defaults, strips unknown properties, and returns a typed value. Throws InstantiationError on failure.
import { InstantiationError } from 'json-tology';
const AddressSchema = {
$id: 'https://bookstore.example/Address',
type: 'object',
properties: {
street: { type: 'string' },
city: { type: 'string' },
postalCode: { type: 'string' },
country: { type: 'string', default: 'US' },
},
required: ['street', 'city', 'postalCode'],
} as const;
const jt2 = jt.register(AddressSchema);
const address = jt2.instantiate(AddressSchema.$id, {
street: '12 Elm Lane',
city: 'Bookham',
postalCode: '94107',
extra: 'ignored', // stripped
// country omitted - default 'US' applied
});
// { street: '12 Elm Lane', city: 'Bookham', postalCode: '94107', country: 'US' }Compose schemas
Compose derives new schemas from existing ones. All composition runs at compile time and produces correct JSON Schema objects.
import { Compose } from 'json-tology';
// A PATCH-body schema where every field is optional
const PatchCustomerSchema = Compose.partial(
CustomerSchema,
'https://bookstore.example/PatchCustomer',
);
// A read-only summary for list views
const CustomerSummarySchema = Compose.pick(
CustomerSchema,
['id', 'name'] as const,
'https://bookstore.example/CustomerSummary',
);See Composition for extend, omit, required, intersection, and discriminatedUnion.
Serialize back to wire form
dump() walks the validation graph and applies any registered Transform encoders. It is the Pydantic model_dump() equivalent.
const customer = jt.instantiate(CustomerSchema.$id, {
id: 'c1a2b3d4-e5f6-7890-abcd-ef1234567890',
email: 'alice@bookstore.example',
name: 'Alice Chen',
});
const wire = jt.dumpJson(CustomerSchema.$id, customer);
// '{"id":"c1a2b3d4-e5f6-7890-abcd-ef1234567890","email":"alice@bookstore.example","name":"Alice Chen","addresses":[]}'See Serialization for filtering options (exclude, include, excludeDefaults).
Sub-path imports
Import only what you need. Every sub-path is tree-shakable.
// Everything
import { JsonTology, Compose, Transform, Value } from 'json-tology';
// Value operations only (no validation graph or ontology)
import { Value, Hash, Changeset } from 'json-tology/value';
// Schema registry and format validators
import { SchemaRegistry, FormatRegistry } from 'json-tology/schema';
// Types and interfaces only (compile-time, no runtime cost)
import type { InferType } from 'json-tology/types';
import type { LoggerInterface } from 'json-tology/interfaces';What's in the box
| Feature | Method(s) |
|---|---|
| Type inference | InferType<T>, InferSchemaType<T, Root> |
| Validation | validate, is, subschemaAt |
| Coercion + defaults | instantiate |
| Error views | aggregate, report |
| Composition | Compose.extend, pick, omit, partial, required, intersection, equivalent, discriminatedUnion |
| Value utilities | Value.clone, hash, diff, value.cast, clean, convert, create |
| Transforms | Transform.create, brand, pipe, jt.encode |
| Serialization | dump, dumpJson |
| Computed fields | addComputed, removeComputed |
| Cross-field invariants | addInvariant, removeInvariant |
| Materialization | materialize |
| RDF/Ontology (advanced, opt-in) | ontology, toQuads, fromQuads, toSchema |
Next steps
| Topic | Guide |
|---|---|
| The running example domain | Bookstore Domain |
| Schemas and registration | Schemas |
| TypeScript type inference | Type Inference |
| Validation and instantiation | Validation |
| Composing schemas | Composition |
| Value operations | Value Operations |
| Transforms and brands | Transforms |
| Serialization | Serialization |
| Computed fields | Computed Fields |
| Cross-field invariants | Invariants |
| RDF/OWL (advanced) | Ontology and Graphs |
All JsonTology.create options
| Option | Type | Default | Purpose |
|---|---|---|---|
baseIRI | string | (required) | Base URI for the canonical graph and ontology output. |
schemas | readonly Schema[] | [] | Schemas to register at construction. Order matters when using $ref - register referenced schemas before referencing schemas. |
prefixes | Record<string, string> | DEFAULT_PREFIXES | Vocabulary prefix → IRI mappings, merged with built-in defaults. |
formats | Record<string, FormatValidatorFn> | {} | Custom format validators. Keys are format names ('isbn'), values are (value: unknown) => boolean. |
enableTypeCast | boolean | false | Enable string→number/boolean coercion at validation time. |
enableStrictTypes | boolean | false | Reject implicit coercions globally. Per-field jt:strict overrides. Different from enableStrictGraph. |
enableDefaults | boolean | true | Fill schema default values during instantiate. Set false to validate without mutating missing fields. |
enableDebug | boolean | false | Surface internal debug logging via logger.debug (graph construction, validator compilation, materialization steps). Useful when investigating unexpected validation outcomes. |
enableInlineWarnings | boolean | false | Surface inline-object, inline-primitive, and inline-array-items warnings via logger.warn at registration. Implied by enableStrictGraph. See graph-native authoring. |
enableDuplicateDetection | boolean | false | Run findDuplicates() at registration and warn on structural duplicates. Implied by enableStrictGraph. |
enableStrictGraph | boolean | false | Promote inline warnings and duplicate detection to SchemaError throws. Requires all sub-schemas to be standalone $id schemas or $defs entries. See strict graph mode. |
keywords | KeywordDefinitionInterface[] | [] | Custom keyword handlers for unrecognized JSON Schema vocabulary. |
vocabularies | VocabularyPluginInterface[] | [] | Vocabulary plugins for custom RDF output (DCAT, FOAF, etc.). |
materializer | MaterializerOptionsInterface | (built-in) | Override the default materializer (rare). |
maxDepth | number | (no limit) | Maximum schema-graph traversal depth. Protects against pathological schemas. |
logger | LoggerInterface | SILENT_LOGGER | Logger for warnings (enableInlineWarnings, enableDuplicateDetection). Must be set for warnings to surface. |
invariants | Record<string, InvariantInterface[]> | {} | Cross-field invariant functions, keyed by schema $id. |
computeds | Record<string, Record<string, ComputedFnType>> | {} | Computed-field functions, keyed by schema $id then property name. |
Graph emission
These options control how toQuads mints subject IRIs and how fromQuads reverses them. See Skolemization for the full reference.
| Option | Type | Default | Purpose |
|---|---|---|---|
iriFor | string | 'blank-node' | SkolemizeFnType | (content-hash) | Default IRI minting strategy for toQuads. A regular string becomes a root-only override; 'blank-node' emits anonymous subjects; a function is the full SkolemizeFnType shape. Per-call options override this. |
defaultGraphIRI | string | (none) | Default graph field for every quad emitted by toQuads. Per-call graphIRI overrides. |
defaultDeskolemize | boolean | false | Treat */.well-known/genid/* IRIs as blank nodes during fromQuads. Reverses Skolemize.wellKnownGenid. |
Related
- Bookstore domain - the running example domain used throughout these docs
- Schemas -
register,has,get,list,toSchema - Picking a method - decision guide for
instantiate,validate,is,materialize
See also
- Argument conventions - universal
SchemaRef, static counterparts - Type Inference - how
InferTyperesolves$ref, enums, brands - Composition - derive schemas with
extend,pick,omit