Skip to content

Schema Derivation

JsonSchema.output derives a JSON Schema from a Spec or Plan without executing it. The schema reflects the output structure, including types, required fields, and nested objects.

import { JsonSchema } from '@origints/core'
// From a Plan
const schema = JsonSchema.output(plan)
// { type: 'object', properties: { name: { type: 'string' }, age: { type: 'number' } }, required: ['name', 'age'] }
import { extract } from '@origints/core'
// From a Spec
const spec = extract($ => ({
name: $.get('name').string(),
age: $.get('age').number(),
}))
const schema = JsonSchema.output(spec)
JsonSchema.output(plan, {
draft: 'draft-07',
id: 'https://example.com/user.schema.json',
title: 'User',
description: 'A user record',
deduplicate: true,
})
OptionEffect
draftJSON Schema dialect: 'draft-07', '2019-09', or '2020-12' (default). Draft-07 uses definitions; 2019-09 and 2020-12 use $defs.
idSets $id on root schema
titleSets title on root schema
descriptionSets description on root schema
deduplicateExtracts repeated sub-schemas into $defs/definitions with $ref pointers

describe(path, meta) attaches annotations to an extraction. These appear in the derived JSON Schema.

.emit((out, $) => out
.add('email', $.get('email').string())
.describe('email', { description: 'User email', format: 'email' })
.add('age', $.get('age').number())
.describe('age', { minimum: 0, maximum: 150 })
)

Supported OutputMeta fields: description, title, examples, deprecated, format, pattern, minimum, maximum, minLength, maxLength, enum.

When deduplicate: true, repeated sub-schemas are extracted into $defs (or definitions for Draft-07) and replaced with $ref pointers. Definitions that contain other definitions use $ref internally — no inline duplication.

Naming strategy:

  1. Property key — the parent property name (e.g., property home → def Home)
  2. Singularized array items — array item schemas use a singularized form (e.g., addressesAddress)
  3. Property-set fallback — object schemas with ≤4 properties use PascalCase property names joined (e.g., NameAge)
  4. Counter fallbackSchema1, Schema2, etc.

$defName promotes a sub-schema to $defs with a specific name, even if it appears only once and even without deduplicate: true. It can be specified in two places:

Use .describe() on an output property. For array properties, $defName names the item schema, not the array itself. Emit-level hints resolve against the post-transform schema, so the definition includes any properties added by subsequent mapOut transforms (e.g., lookups).

.emit((out, $) => out
.add('orders', $.get('orders').array(o => ({ ... })))
.describe('orders', { $defName: 'Order' })
// → orders.items becomes { $ref: '#/$defs/Order' }
)

Use the $defName option on output transforms that produce new named schema shapes: lookup, indexBy, groupBy, nest, and joinBy.

.mapOut($ => $.lookup(o => o.orders, {
from: 'customers',
localKey: 'customerId',
foreignKey: 'cid',
as: 'customer',
cardinality: 'one',
$defName: 'Customer',
}))
.mapOut($ => $.indexBy(o => o.users, 'id', { $defName: 'User' }))
.mapOut($ => $.groupBy(o => o.items, 'cat', { $defName: 'Item' }))
.mapOut($ => $.nest('address', ['street', 'city'], { $defName: 'Address' }))
TransformWhat $defName names
lookup (one)The source item schema (inside anyOf: [schema, null])
lookup (many)The source item schema (inside items)
lookup (merge)Not applicable (fields are spread directly)
indexByThe item schema (inside additionalProperties)
groupBy (record)The item schema (inside additionalProperties.items)
groupBy (entries)The entry object schema {key, items}
nestThe nested object schema
joinByThe joined item object schema

When both emit-level and transform-level hints exist, they are merged. Transform-level hints take precedence for the same schema.

SpecSchema
extract (string/number/boolean/null/date/datetime){ type: T } with optional default
literal{ const: value }
array{ type: 'array', items: ... }
object{ type: 'object', properties: ..., required: [...] }
matchUnion of branch schemas via anyOf or type merge
concat{ type: 'array', items: merged }
tryUnion of all fallback schemas
mapSchema of the source spec
guardSchema of the source spec
panic{} (never produces output)