Combining Keywords
Following the JSON Schema spec, all keywords are evaluated simultaneously. When multiple keywords are present (type, properties, oneOf, allOf, if/then/else, patternProperties), they create intersecting constraints that must all be satisfied — and the inferred type reflects every one of them.
How it works
const combinedSchema = new SchemaBuilder()
.object()
.properties({
id: (s) => s.number(),
name: (s) => s.string(),
})
.required(["id"])
.allOf(
(s) =>
s.object().properties({ createdAt: (s) => s.number() }).required(["createdAt"]),
)
.oneOf(
(s) =>
s
.object()
.properties({
type: (s) => s.const("user"),
email: (s) => s.string(),
})
.required(["type", "email"]),
(s) =>
s
.object()
.properties({
type: (s) => s.const("admin"),
permissions: (s) => s.array().items((s) => s.string()),
})
.required(["type", "permissions"]),
)
.build();
type Combined = Jet.Infer<typeof combinedSchema>;
// {
// id: number; createdAt: number; type: "user"; email: string; name?: string;
// } | {
// id: number; createdAt: number; type: "admin"; permissions: string[]; name?: string;
// }
// Base props (id, name) and allOf props (createdAt) appear in BOTH branches;
// oneOf creates the discriminated split.Layering combinators — counting branches
With if/then only (no else), the then is merged into each oneOf branch as a single branch: 2 oneOf Ă— 1 = 2 final branches.
const ifThenOnly = new SchemaBuilder()
.allOf((s) =>
s
.object()
.properties({ createdAt: (s) => s.number(), updatedAt: (s) => s.number() })
.required(["createdAt", "updatedAt"]),
)
.oneOf(
(s) =>
s.object().properties({ recordType: (s) => s.const("invoice"), amount: (s) => s.number() }).required(["recordType", "amount"]),
(s) =>
s.object().properties({ recordType: (s) => s.const("receipt"), amount: (s) => s.number() }).required(["recordType", "amount"]),
)
.if((s) => s.object().properties({ amount: (s) => s.number().minimum(1000) }))
.then((s) =>
s.object().properties({ requiresApproval: (s) => s.const(true), approver: (s) => s.string() }).required(["requiresApproval", "approver"]),
)
.end()
.build();
type IfThenOnly = Jet.Infer<typeof ifThenOnly>;
// 2 branches (invoice / receipt), each with requiresApproval: true; approver: stringAdd an .else() and the conditional becomes two outcomes, multiplying through each oneOf branch: 2 oneOf Ă— 2 (then / else) = 4 final branches.
const ifThenElse = new SchemaBuilder()
.allOf((s) =>
s
.object()
.properties({ createdAt: (s) => s.number(), updatedAt: (s) => s.number() })
.required(["createdAt", "updatedAt"]),
)
.oneOf(
(s) =>
s.object().properties({ recordType: (s) => s.const("invoice"), amount: (s) => s.number() }).required(["recordType", "amount"]),
(s) =>
s.object().properties({ recordType: (s) => s.const("receipt"), amount: (s) => s.number() }).required(["recordType", "amount"]),
)
.if((s) => s.object().properties({ amount: (s) => s.number().minimum(1000) }))
.then((s) =>
s.object().properties({ requiresApproval: (s) => s.const(true), approver: (s) => s.string() }).required(["requiresApproval", "approver"]),
)
.else((s) =>
s.object().properties({ requiresApproval: (s) => s.const(false) }).required(["requiresApproval"]),
)
.build();
type IfThenElse = Jet.Infer<typeof ifThenElse>;
// 4 branches: { invoice | receipt } Ă— { requiresApproval: true; approver } | { requiresApproval: false }Branch math: oneOf branches Ă— conditional outcomes. if/then alone is one outcome;
adding else makes two.
Properties + pattern properties
const fullPropertiesSchema = new SchemaBuilder()
.object()
.properties({
id: (s) => s.number(),
name: (s) => s.string(),
})
.required(["id"])
.patternProperties({
"^meta_": (s) => s.string(),
"^flag_": (s) => s.boolean(),
})
.build();
type FullProperties = Jet.Infer<typeof fullPropertiesSchema>;
// {
// id: number; name?: string;
// [key: `meta_${string}`]: string;
// [key: `flag_${string}`]: boolean;
// }Object + array types together
const multiObjectArraySchema = new SchemaBuilder()
.object()
.properties({ name: (s) => s.string() })
.array()
.items((s) => s.number())
.build();
type MultiObjectArray = Jet.Infer<typeof multiObjectArraySchema>;
// { name?: string } | number[]Boolean schemas
const anySchema = new SchemaBuilder().properties({ name: true }).build();
type Any = Jet.Infer<typeof anySchema>;
// name accepts anything
const noneSchema = new SchemaBuilder().properties({ name: false }).build();
type None = Jet.Infer<typeof noneSchema>;
// name must not exist — { name: "" } always failsEvery keyword you add is an additive constraint. TypeScript combines them all to give the most precise type, mirroring exactly what the runtime validator enforces.