zmdbzero-maintenance data layer
Docs Benchmarks Anti-patterns OpenAPI
Docs / Ecosystem integrations

LLM SchemasSupported

Install: yarn add @zmdb/schema-core@1.0.0-beta.2 for runtime schema-to-JSON-Schema conversion; run yarn add @zmdb/ai@1.0.0-beta.2 for provider tool documents. JSON Schema ownership stays in schema-core, while provider dialects live in the provider-neutral AI package.

toJsonSchema turns a schema object — the value schemaOf<T>() produces — into a JSON Schema document. That is the currency every LLM API and every other validation library speaks, so it is the bridge out of zmdb's type system.

Basic use#

import { toJsonSchema } from '@zmdb/schema/openapi';
import { userSchema } from './schema.js';

const schema = toJsonSchema(userSchema, 'entity');
{
  "type": "object",
  "properties": {
    "id": { "type": "integer" },
    "email": { "type": "string" },
    "active": { "type": "boolean" }
  },
  "required": ["id", "email", "active"]
}

Variants#

The second argument picks which shape of the schema you want, and they differ in exactly the ways the DTO types differ:

VariantContains
'entity'every column — the full row
'create'omits Serial; HasDefault and nullable columns optional
'update'every column optional
'get'the lookup shape (select, populate)
'list'the list envelope (where, orderBy, page, select)
'search'the full-text search shape
toJsonSchema(userSchema, 'create'); // what a POST body must look like
toJsonSchema(userSchema, 'update'); // what a PATCH body may look like

toListSchema(userSchema) and toSearchSchema(userSchema) are the direct forms of the last two.

Validation tags become constraints#

A validation tag on a column is metadata, and this is where it pays off:

email: string & Sql<'text'> & Pattern<'^[^@]+@[^@]+$'>;
age: (number & Sql<'integer'> & Max<120>) | null;
{
  "email": { "type": "string", "pattern": "^[^@]+@[^@]+$" },
  "age": { "type": ["integer", "null"], "maximum": 120 }
}

So a constraint written once shapes the emitted validator, the OpenAPI document, the LLM tool schema and any consumer generating a form — without being re-declared anywhere. The five keywords that travel this far are minimum, maximum, minLength, maxLength and pattern; Rule<'name'> reaches the column IR but has no JSON Schema counterpart.

Sensitive columns are omitted#

passwordHash: string & Sql<'text'> & Sensitive;

Absent from the output, in every variant — create included. The filter runs at the last step before a document is produced, so no derived type a caller invents routes around it. That is what makes it safe to hand a derived schema to a model or publish it in a document. Note that Entity<User> and CreateDTO<User> still carry the column: the tag is about what leaves, not about what the row is.

Relations#

import { toJsonSchemaWithRelations } from '@zmdb/schema/openapi';

toJsonSchemaWithRelations(userSchema, 'entity');

Adds a $ref per relation the type declares — posts?: Post[] & OneToMany<'posts', 'authorId'> becomes an array of the target's entity schema, which is the shape a populate actually returns, so the document matches the response. Relations reach the entity variant only: a create body is columns, and a nested entity in one would advertise a write path that does not exist.

Every schema at once#

import { toOpenApiComponents } from '@zmdb/schema/openapi';

const components = toOpenApiComponents([userSchema, postSchema, commentSchema]);

Keyed by table name and variant, ready to drop into an OpenAPI document's components.schemas, or to serve as a manifest of your data model. You supply the array — nothing enumerates your tables, because a type cannot register itself. See OpenAPI.

Feeding it to a model#

The direct use is a structured-output constraint:

const res = await fetch('https://api.anthropic.com/v1/messages', {
  method: 'POST',
  headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },
  body: JSON.stringify({
    model: 'claude-opus-5',
    max_tokens: 1024,
    tools: [{ name: 'save_user', description: 'Save a user', input_schema: toJsonSchema(userSchema, 'create') }],
    tool_choice: { type: 'tool', name: 'save_user' },
    messages: [{ role: 'user', content: text }],
  }),
});

The model's output is now constrained to the shape your database accepts — and toolFromSchema wraps exactly this. See Structured Output.

Out to other validators#

Zod, Valibot, TypeBox and ArkType all import JSON Schema, so this is the interop path:

import { jsonSchemaToZod } from 'json-schema-to-zod';
const zodSchema = jsonSchemaToZod(toJsonSchema(userSchema, 'create'));

You do not need this for validation — the AOT validators work from the TypeScript type directly — but it is how you hand a shape to a library that is already in your stack. See Zod.

---

See also: Structured Output · OpenAPI · Zod