json-tology
One source of truth for TypeScript types, runtime validation, coercion, and OWL ontology output.
Author in JSON Schema. Share with any backend. Reason over the graph.
Compile-time Types
InferType<typeof Schema> derives the TypeScript type from your as-const schema literal. No code generation, no separate type definition.
Runtime Validation
validate(), is(), errors() check data against a registered schema and return structured ValidationErrors with JSON Pointer paths.
Coercion with Defaults
coerce() validates, applies defaults, runs Transform decoders, and strips unknown properties in one pass. Throws a typed InstantiationError on failure.
Composition
extend, pick, omit, partial, required, intersection, discriminatedUnion: Pydantic-style derivations from base schemas, all type-safe.
Transforms and Brands
Per-field decode/encode pipelines via Transform, plus brand types for nominal typing. Round-trip wire format to decoded values with type safety.
Tree-Shakable
Sub-path exports - json-tology/value, json-tology/schema, json-tology/types - let users opt into only what they need. Graph and ontology are fully tree-shakable.
JSON Schema is the source, not an output
Most TypeScript validation libraries treat JSON Schema as a side-effect. You write Zod or TypeBox or Valibot, and toJSONSchema(...) is a one-way export. The exported schema is a snapshot, not a contract: regenerate it on every refactor, hope your back-end picks up the change, hope no one edits the JSON copy by hand.
json-tology inverts this. The JSON Schema literal is the schema. Type inference is derived from it. Validation reads it. Coercion reads it. The OWL TBox reads it. The same as const object you authored in TypeScript is also a wire-format-compatible JSON Schema document, ready to ship to any consumer in any language.
const CustomerSchema = {
$id: 'urn:bookstore:Customer',
type: 'object',
properties: {
id: { $ref: 'urn:bookstore:CustomerId' },
email: { $ref: 'urn:bookstore:Email' },
name: { $ref: 'urn:bookstore:PersonName' }
},
required: ['id', 'email', 'name']
} as const;That literal is:
- A TypeScript type via
InferType<typeof CustomerSchema> - A runtime validator via
entities.validate(CustomerSchema, data) - An OpenAPI 3.1 component (paste it into
components.schemas.Customer) - A draft-2020-12 JSON Schema (Ajv, FastValidator, fastify-json-schema, any conforming validator reads it directly)
- An OWL class (via
entities.toTbox()) - A SHACL shape (via
entities.toShacl()) - A documentation source for tools like
@apidevtools/json-schema-ref-parser,redoc,swagger-ui
Cross-language interop, no codegen
Sharing a contract with a Python back-end, a Go service, a Rust validator, or a Java reasoner is JSON.stringify(CustomerSchema). There is no generator step. There is no regeneration on every refactor. The TS type and the wire schema can't drift because they are the same object.
| Library | TS type | Runtime validator | Wire-format JSON Schema | OWL/SHACL output |
|---|---|---|---|---|
| Zod | source | yes | export-only via zod-to-json-schema (lossy) | no |
| TypeBox | source (via Static<>) | yes (Value.Check) | yes (TypeBox schemas ARE JSON Schema) | no |
| Valibot | source | yes | export-only via @valibot/to-json-schema | no |
| Pydantic | source | yes | export-only via model_json_schema() | no |
| Ajv | no TS inference | yes | source (raw JSON Schema author) | no |
| json-tology | derived from source | yes | source | yes (TBox + SHACL) |
TypeBox is the closest comparator: TypeBox schemas are also JSON-Schema-compatible objects. The differences: TypeBox doesn't ship runtime registration, cross-schema $ref resolution, ABox projection, OWL/SHACL output, or graph-native authoring tools.
Advanced usages
Your types are already a graph
Every TypeScript type system has a graph hiding in it. Below is the bookstore domain - six entities, eighteen primitives, every property a typed edge. Nodes are classes; edges are properties; arrowheads point from the domain entity to the range type.
Why json-tology
If you're coming from Pydantic, Zod, or TypeBox, json-tology gives you the same authoring ergonomics with JSON Schema as the source of truth - your schema works in TypeScript, in JSON Schema validators, in OpenAPI, in IDE auto-complete, and as a wire-format contract, all from one declaration.
import { JsonTology } from 'json-tology';
import type { InferType } from 'json-tology/types';
const CustomerSchema = {
$id: 'https://bookstore.example/Customer',
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
email: { type: 'string', format: 'email' },
name: { type: 'string' },
addresses: { type: 'array', items: { type: 'object' }, default: [] },
},
required: ['id', 'email', 'name'],
} as const;
type Customer = InferType<typeof CustomerSchema>;
// ^? { readonly id: string; readonly email: string; readonly name: string; readonly addresses?: readonly object[] }
const jt = JsonTology.create({
baseIRI: 'https://bookstore.example',
schemas: [CustomerSchema] as const,
});
const customer = jt.instantiate(CustomerSchema.$id, {
id: 'c1a2b3d4-e5f6-7890-abcd-ef1234567890',
email: 'alice@bookstore.example',
name: 'Alice Chen',
});
// ^? Customer - typed, validated, defaults appliedThat's the entire core. Validation, type inference, coercion, defaults - all from one schema literal.
What's in the box
| You get | Without paying for |
|---|---|
Type inference (InferType) | A separate type-definition language |
Runtime validation (validate, is) | A second schema for runtime checks |
Coercion + defaults (instantiate) | Manual mapping of input shapes |
Field aliasing (jt:alias) | Custom transform layers for renames |
Computed fields (jt:computed) | Post-processing pipelines |
Cross-field invariants (addInvariant) | Custom validation glue |
Serialization (dump, dumpJson) | A separate serializer |
Composition (extend, pick, omit, partial, required) | Hand-written derived schemas |
W3C / RDF / OWL / SHACL conformance is aspirational and a work in progress. Output loads into reasoners like Apache Jena and editors like Protege, but full normative conformance is still being built out. See the References page.
If you also need RDF/OWL/SHACL output, that's available as opt-in advanced features under the Ontology and Graphs section. The core type-system path doesn't pay for any of it - json-tology/value, json-tology/schema, and json-tology/types exclude the graph and ontology modules entirely.
Quick links
- Getting Started - install, define a schema, validate, instantiate
- Bookstore Domain - the running example domain used throughout the docs
- Validation -
instantiate,validate,is,subschemaAt - Error Views -
aggregate,report(RFC 7807) - Type Inference - how
InferTypeworks, reference maps, branded types - Composition - derive schemas from other schemas
- Serialization -
dump,dumpJson, Transform encoders - Ontology and Graphs - advanced: OWL TBox, SHACL shapes, JSON-LD, ABox projection
Related
- Getting Started - install, validate, instantiate in 5 minutes
- Bookstore domain - the running example domain used throughout
- Picking a method - instantiate vs validate vs is vs materialize
See also
- Argument conventions - universal SchemaRef, static counterparts
- Composition - derive schemas with extend, pick, omit
- Ontology and Graphs - advanced: OWL TBox, SHACL shapes, JSON-LD