Schema API#

Everything typed in Janux uses one schema system, importable from janux.

Builders#

Builder JSON Schema projection Zero value
str() { type: 'string' } ''
int() { type: 'integer' } 0
num() { type: 'number' } 0
bool() { type: 'boolean' } false
money() { type: 'integer', format: 'money-minor-units' } 0
enums(['a','b']) { enum: ['a','b'] } first value
list(shapeOrType) { type: 'array', items } []
obj({...}) / schema({...}) { type: 'object', properties, required } recursive

list({ id: str() }) is shorthand for list(obj({ id: str() })).

Modifiers#

Chainable, immutable — each returns a new type:

str().nullable()          // null allowed; JSON Schema type becomes ['string','null']
int().optional()          // may be missing
int().default(1)          // applied when missing; also removes it from `required`
int().min(1).max(99)      // value bounds (length bounds for strings)
str().options(({ state }) => state.drafts.map((d) => d.id))  // values accepted right now

options(resolve)#

The values a field accepts right now, resolved against the live instance when the manifest is built — the value-level twin of an intent's ready.

send: intent({
  guard: 'confirm',
  input: schema({ id: str().default('pay_acme').options(({ state }) => pendingIds(state)) }),
  run: ({ state, input }) => pay(state, input.id),
});
// manifest: { type: 'string', default: 'pay_acme', enum: ['pay_lumen'] }

An agent reading the manifest sees the ids that still exist instead of guessing one that already went out. Notes:

  • Advisory, not validation. The enum only rides the published JSON Schema; validate is unchanged, so a list that goes stale never rejects a caller — your run() stays the authority on what is possible.
  • Resolved per instance: with no instance (a schema-only projection via toJsonSchema), an empty list, or a resolver that throws, the published schema is left exactly as declared.
  • Keep a .default() too: it is what the tool means in general, the enum is what it takes today.

validate(type, value)#

const result = validate(cartSchema, input);
// { ok: boolean, value: unknown, errors: [{ path, message }] }
  • Missing value: .default() wins → optional passes undefinednullable passes nullrequired error.
  • Unknown object keys are stripped, never passed through.
  • Error paths are precise: items[0].qty: below min 1.

The framework calls this for you on every intent input, api input/output and event payload. You call it directly only for custom validation flows.

coerceForm(value, type)#

The pre-processing behind an intent's coerce: 'form': converts the strings FormData submits into what a typed schema means, without validating.

coerceForm({ attendees: '3', optIn: 'on' }, schema({ attendees: int(), optIn: bool() }));
// { attendees: 3, optIn: true }
  • int() / num() / money() — parsed with Number; a blank or non-numeric string is left alone (so validate still rejects it), and money() is never scaled.
  • bool() — checkbox semantics: 'on'/'true'true, absent → false.
  • str() / enums() and already-typed values — untouched.

You call it directly only when converting form drafts yourself (say, live per-field validation before submit); intents with coerce: 'form' run it for you before validate.

buildDefault(type)#

Builds initial state: explicit defaults, null for nullables, [] for lists, first enum value, zero values for primitives. This is how a component boots when no initial is provided.

toJsonSchema(type)#

Serializes to standard JSON Schema — what the manifest publishes for every tool input and resource schema, so external MCP clients and LLMs validate against the same contract the server enforces.

JxType#

The value every builder returns — the runtime type object the framework carries around. You never construct it by hand (str(), int(), obj()… do that), but you'll name it when a helper takes a schema:

import { str, type JxType } from 'janux';

function field(name: string, type: JxType) {
  return { name, json: toJsonSchema(type) };
}

field('email', str().min(3));

It's immutable: .optional(), .nullable(), .default(v) and .options(fn) return a new JxType with the extra flag, which is why a shared base type can be reused without a modifier on one field leaking into another.

const id = str();
const maybeId = id.optional();   // `id` is unchanged

A type exposes kind ('str', 'int', 'obj', 'list'…), flags (optional / nullable / default) and, where relevant, values (enums), item (lists) or shape (objects) — the same fields toJsonSchema and urlState read.