Skip to content

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.

/**
 * Landing page: JSON Schema is the source, not an output
 *
 * The canonical `CustomerSchema` is:
 *
 * - A TypeScript type via `InferType<typeof CustomerSchema>`
 * - A runtime validator via `entities.validate(CustomerSchema, data)`
 * - An OpenAPI 3.1 component (paste into `components.schemas.Customer`)
 * - A JSON Schema draft 2020-12 document (any conforming validator reads it)
 * - An OWL class (via `entities.toTbox()`)
 * - A SHACL shape (via `entities.toShacl()`)
 *
 * The same `as const` object that types your TypeScript is also the
 * wire-format contract for every consumer.
 */

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

// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();

// The schema literal is also the validator — no separate declaration file.
const errs = jt.validate(CustomerSchema, aboxFixtures.customer);

console.assert(errs.length === 0);

const schemaId: string = CustomerSchema.$id;

console.assert(schemaId === 'urn:bookstore:Customer');

// The same schema literal serializes to valid JSON Schema 2020-12.
const asJson = JSON.stringify(CustomerSchema);

console.assert(asJson.includes('"$id"'));
console.assert(asJson.includes('urn:bookstore:Customer'));

console.log('schema $id:', schemaId);
console.log('validate error count:', errs.length);
console.log('serialized includes $id:', asJson.includes('"$id"'));
Output
Press Execute to run this example against the real library.

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 JSON Schema draft 2020-12 document (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

All examples in this documentation target the JSON Schema draft 2020-12 dialect.

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.

The same property holds for LLM consumers: every major model provider has converged on JSON Schema for structured outputs and tool calling, so a json-tology schema literal drops directly into an OpenAI or Anthropic function definition - see Sourcemeta's AI only speaks JSON Schema for the full argument. The same as const object that types your TypeScript also defines the model's output contract.

LibraryTS typeRuntime validatorWire-format JSON SchemaOWL/SHACL output
Zodsourceyesexport-only via zod-to-json-schema (lossy)no
TypeBoxsource (via Static<>)yes (Value.Check)yes (TypeBox schemas ARE JSON Schema)no
Valibotsourceyesexport-only via @valibot/to-json-schemano
Pydanticsourceyesexport-only via model_json_schema()no
Ajvno TS inferenceyessource (raw JSON Schema author)no
json-tologyderived from sourceyessourceyes (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.

Nodes

  • Entity — registered top-level class (Customer, Order, Book, Review, …)
  • Primitive — named scalar / constrained type (Email, Iso8601, Amount, …)

Edges

  • subClassOfrdfs:subClassOf (taxonomic narrowing)
  • equivalentClassowl:equivalentClass (alias / structural identity)
  • rangerdfs:range (property → typed target)
  • domainrdfs:domain (explicit override)
  • disjointWithowl:disjointWith (no shared instances)
  • complementOfowl:complementOf (negation)
  • restrictionowl:Restriction (cardinality / hasValue / …)
Loading graph data...

Read the full guide


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.

/**
 * Landing page: core workflow — schema → type → validate → instantiate
 *
 * The entire core in one file: declare a schema, derive the TypeScript type,
 * create a registry, validate and instantiate. Bastian Balthazar Bux is the
 * canonical bookstore customer (from Michael Ende's *The Neverending Story*).
 */

import { JsonTology } from '../../../src/index.js';
import type { InferType } from '../../../src/types/index.js';

const CustomerSchema = {
  '$id': 'urn:landing-02:Customer',
  'properties': {
    'addresses': {
      'default': [],
      'items': { 'type': 'object' },
      'type': 'array'
    },
    'email': {
      'format': 'email',
      'type': 'string'
    },
    'id': {
      'format': 'uuid',
      'type': 'string'
    },
    'name': { 'type': 'string' }
  },
  'required': [
    'id',
    'email',
    'name'
  ],
  'type': 'object'
} as const;

type Customer = InferType<typeof CustomerSchema>;

const jt = JsonTology.create({
  'baseIri': 'urn:landing-02',
  // doc example with synthetic fixture schemas
  'enableStrictGraph': false,
  'schemas': [CustomerSchema] as const
});

const customer: Customer = jt.instantiate(CustomerSchema.$id, {
  'email': 'bastian.bux@bookstore.example',
  'id': 'c1a2b3d4-e5f6-7890-abcd-ef1234567890',
  'name': 'Bastian Balthazar Bux'
});

// Typed, validated, defaults applied.
console.assert(customer.name === 'Bastian Balthazar Bux');
console.assert(Array.isArray(customer.addresses));

console.log('customer.name:', customer.name);
console.log('customer.email:', customer.email);
console.log('customer.addresses (default):', JSON.stringify(customer.addresses));
Output
Press Execute to run this example against the real library.

That's the entire core. Validation, type inference, coercion, defaults - all from one schema literal.

What's in the box

You getWithout 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.

See also

Released under the MIT License.