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 nowoptions(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;
validateis unchanged, so a list that goes stale never rejects a caller — yourrun()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 →optionalpassesundefined→nullablepassesnull→requirederror. - 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 withNumber; a blank or non-numeric string is left alone (sovalidatestill rejects it), andmoney()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 unchangedA 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.