Skip to content

Your types are already a graph

Every non-trivial type system has a graph hiding in it. Here's the bookstore domain - six entity classes (Customer, Order, Book, OrderLine, Address, Review), eighteen atomic primitives (Isbn, Email, Money, …), every property a typed edge from one entity to another.

You don't need json-tology to have this graph. You already do - it's there in your TypeScript interfaces, your Pydantic models, your Zod schemas, your protobuf definitions, your JSON Schema documents. The shape of your domain is a directed graph of types referencing types, whether you draw it or not.

What json-tology adds is semantic legibility: the graph becomes machine-queryable. The same $ref you use for type inference becomes an rdfs:subClassOf edge. The same enum you use for unions becomes an owl:oneOf set. The same pattern you use for input validation becomes a sh:pattern constraint. None of these is new information - it's all already in the schema. json-tology just emits it in a vocabulary that machines (reasoners, query engines, other ontology tools) can read.

Below is the bookstore TBox rendered with Cytoscape. Click any node to inspect its schema. Edges are property names; arrowheads point from the property's domain (the entity that has the property) to its range (the type the property refers to).

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

Branding: same validation, different concepts

PersonName is the canonical primitive: { type: 'string', minLength: 1, maxLength: 200 }. CustomerName and AuthorName are both sibling extensions of PersonName - they share the same validation rule, but they are domain-distinct: one belongs to customer identity, the other to book authorship. Mixing them in code would be a type error.

Compose.equivalent creates each as a thin $ref over PersonName:

/**
 * Your types are a graph: Compose.equivalent — domain branding
 *
 * `PersonName` is the canonical primitive: `{ type: 'string', minLength: 1,
 * maxLength: 200 }`. `CustomerName` and `AuthorName` are both sibling
 * extensions — they share the same validation rule but are domain-distinct.
 *
 * `Compose.equivalent` creates each as a thin `$ref` over `PersonName`:
 * one compiled validator (no duplication), three class IRIs, and
 * `owl:equivalentClass` arcs in the TBox.
 *
 * The canonical bookstore exports all three. This example exercises the
 * runtime: all three validate the same string, but the TypeScript type
 * system keeps them nominally distinct.
 */

import {
  AuthorNameSchema,
  bookstoreEntities,
  CustomerNameSchema,
  PersonNameSchema
} from '../bookstore/index.js';

// All three share the same underlying validation rule.
const name = 'Cornelia Funke';

const errsPersonName = bookstoreEntities.validate(PersonNameSchema.$id, name);
const errsCustomerName = bookstoreEntities.validate(CustomerNameSchema.$id, name);
const errsAuthorName = bookstoreEntities.validate(AuthorNameSchema.$id, name);

console.assert(errsPersonName.length === 0);
console.assert(errsCustomerName.length === 0);
console.assert(errsAuthorName.length === 0);

console.log('PersonName validates:', errsPersonName.length === 0);
console.log('CustomerName validates:', errsCustomerName.length === 0);
console.log('AuthorName validates:', errsAuthorName.length === 0);

// Three distinct IRIs — three class nodes in the TBox. Each `$id` is a
// distinct string literal type, so distinctness is guaranteed at compile time:
// `extends` between any pair resolves to `false`.
type PersonVsCustomer = typeof PersonNameSchema.$id extends typeof CustomerNameSchema.$id ? false : true;
type PersonVsAuthor = typeof PersonNameSchema.$id extends typeof AuthorNameSchema.$id ? false : true;
type CustomerVsAuthor = typeof CustomerNameSchema.$id extends typeof AuthorNameSchema.$id ? false : true;
const _distinctIds: [PersonVsCustomer, PersonVsAuthor, CustomerVsAuthor] = [
  true,
  true,
  true
];

console.assert(_distinctIds.every(Boolean));

console.log('PersonNameSchema.$id:', PersonNameSchema.$id);
console.log('CustomerNameSchema.$id:', CustomerNameSchema.$id);
console.log('AuthorNameSchema.$id:', AuthorNameSchema.$id);
console.log('distinct IRI types:', _distinctIds);
Output
Press Execute to run this example against the real library.

The result: one compiled validator (no duplication), three class IRIs, and owl:equivalentClass arcs in the TBox - visible as the green dashed edges in the graph above. Ontology-aware tools can infer that any CustomerName or AuthorName is also a valid PersonName and vice versa, while your TypeScript types keep the three concepts nominally distinct.


What this means

You don't have to use the graph features to use json-tology. Most consumers will use validate() and instantiate() and never look at the TBox. That's fine - the graph is metadata, not the runtime.

But the graph is there either way. When you decide to query it (with entities.toTbox()), or visualize it in a different tool (with entities.toTbox().jsonLd()), or reason over it with an OWL inferrer, the same model that drives validation drives those workflows too. You don't get a second model. There's just one.

Read the graph concepts guide → - TBox/ABox, OWA, subClassOf, equivalentClass, the full conceptual coverage.


Legend

Nodes

ColorMeaning
Dark blue (#005a9c)Entity schema - has object properties (Book, Customer, Order, ...)
Light blue (#a8d1f0)Primitive schema - a named leaf type (Isbn, Email, Money, ...)

Edges

StyleKindMeaning
Solid gray arrowsubClassOfSource is a subclass of target (from Compose.extend)
Green dashedequivalentClassLogically identical classes (from Compose.equivalent)
Solid blue veerangeA property of the source points to the target class
Dotted orangedomainExplicit rdfs:domain override (rare, usually inferred)

See also

Released under the MIT License.