@jetio/schema-builder
v1.1.0
Published
Fluent, type-safe JSON Schema builder with spec compliant type inference for TypeScript. Works with @jetio/validator.
Maintainers
Readme
📐 Schema Builder Guide
The Schema Builder provides a fluent, type-safe API for constructing JSON Schemas programmatically. Build complex schemas with autocomplete, validation, and zero boilerplate and get automatic type inference.
Note: This package includes @jetio/validator as a dependency. You get both the builder AND the fastest JSON Schema validator in one install.
Important: Jetio/schema-builder is a JSON Schema spec-compliant tool built on top of @jetio/validator. To utilize it to its fullest potential, it's essential to understand the main validator package. All core documentation about validation rules, error handling, $data references, and advanced features can be found in the Validator Documentation.
📦 Installation
npm install @jetio/schema-builder
# or
yarn add @jetio/schema-builder
# or
pnpm add @jetio/schema-builderimport { SchemaBuilder, RefBuilder } from "@jetio/schema-builder";
import { JetValidator } from "@jetio/schema-builder"; // Re-exported from @jetio/validatorJust need the validator? Install @jetio/validator directly for a smaller bundle.
Type Inference
JetIO Schema Builder includes Json Schema spec compliant automatic TypeScript type inference through Jet.Infer<>. Write your schema once and get both runtime validation AND compile-time types!
import { SchemaBuilder, Jet } from "@jetio/schema-builder";
const userSchema = new SchemaBuilder()
.object()
.properties({
id: (s) => s.number(),
name: (s) => s.string(),
email: (s) => s.string().format('email')
})
.required(['id', 'name', 'email'])
.build();
// Automatically infer TypeScript type from schema
type User = Jet.Infer<typeof userSchema>;
/*
{
id: number;
name: string;
email: string;
}
*/
// Type-safe usage
const user: User = {
id: 1,
name: "Alice",
email: "[email protected]"
}; // ✅
const invalidUser: User = {
id: 1,
name: "Bob"
// ❌ TypeScript Error: Property 'email' is missing
};Why Type Inference Matters
- Single Source of Truth: Schema and types never drift out of sync
- Automatic Updates: Change the schema, types update automatically
- Runtime + Compile Time Safety: Catch errors at both stages
- Zero Duplication: No need to write interfaces separately
Advanced Type Inference
Type inference works with all schema features:
// Discriminated unions
const shapeSchema = new SchemaBuilder()
.oneOf(
(s) => s.object()
.properties({
type: (s) => s.const('circle'),
radius: (s) => s.number()
})
.required(['type', 'radius']),
(s) => s.object()
.properties({
type: (s) => s.const('rectangle'),
width: (s) => s.number(),
height: (s) => s.number()
})
.required(['type', 'width', 'height'])
)
.build();
type Shape = Jet.Infer;
// { type: 'circle'; radius: number } | { type: 'rectangle'; width: number; height: number }
// Conditionals with elseIf
const accountSchema = new SchemaBuilder()
.object()
.properties({
accountType: (s) => s.string(),
username: (s) => s.string(),
companyName: (s) => s.string()
})
.required(['accountType'])
.if((s) => s.object().properties({ accountType: (s) => s.const('personal') }))
.then((s) => s.object().required(['username']))
.elseIf((s) => s.object().properties({ accountType: (s) => s.const('business') }))
.then((s) => s.object().required(['companyName']))
.end()
.build();
type Account = Jet.Infer;
// TypeScript understands the conditional branches!📖 For complete type inference documentation, see Type Inference Guide
Topics covered in the Type Inference guide:
- Primitives, objects, arrays, and their type inference
- Pattern properties with template literal types
- Multiple types and union inference
- Discriminated unions with oneOf/anyOf
- Conditional type inference (if/then/else/elseIf)
- Complex compositions with allOf
- Required vs optional property splitting
- Type inference limitations and workarounds -addtionalItems/Properties, unevaluatedProperties/Items, patternProperties.
🎯 Quick Start
import { SchemaBuilder } from "@jetio/schema-builder";
// Simple schema
const userSchema = new SchemaBuilder()
.object()
.properties({
name: s => s.string().minLength(2),
age: s => s.number().minimum(18)
})
.required(['name', 'age'])
.build();
// Equivalent to:
{
type: "object",
properties: {
name: { type: "string", minLength: 2 },
age: { type: "number", minimum: 18 }
},
required: ["name", "age"]
}📚 Table of Contents
- Basic Types
- TypeScript Type Locking
- String Schemas
- Number Schemas
- Array Schemas
- Object Schemas
- Boolean Schema Values
- Composition (allOf, anyOf, oneOf)
- Conditionals (if/then/else)
- References ($ref, $dynamicRef)
- Definitions ($defs)
- Custom Error Messages
- Advanced Features
- Schema Reuse & Extension
- Mixed Approaches
Draft Support
The Schema Builder supports JSON Schema keywords from Draft 06 through Draft 2020-12, including:
- Draft 06/07:
definitions,dependencies,items(array form),additionalItems - Draft 2019-09:
$defs,dependentSchemas,dependentRequired,unevaluatedProperties,unevaluatedItems - Draft 2020-12:
prefixItems,$dynamicRef,$dynamicAnchor - Extension:
elseIf(Jet-Validator extension for cleaner conditionals)
Use the appropriate keywords for your target draft version. The builder doesn't enforce draft restrictions — you can mix keywords, but ensure your validator supports them.
The builder follows json schema rules which means a schema can either be a standard json schema or a boolean.
Basic Types
Simple Type Declarations
// String
const nameSchema = new SchemaBuilder().string().build();
// { type: "string" }
// Number
const ageSchema = new SchemaBuilder().number().build();
// { type: "number" }
// Integer
const countSchema = new SchemaBuilder().integer().build();
// { type: "integer" }
// Boolean
const activeSchema = new SchemaBuilder().boolean().build();
// { type: "boolean" }
// Null
const nullSchema = new SchemaBuilder().null().build();
// { type: "null" }Multiple Types
// Chain type methods to create type arrays
const flexibleSchema = new SchemaBuilder().string().number().build();
// { type: ["string", "number"] }
// Nullable values
const nullableString = new SchemaBuilder().string().null().minLength(5).build();
// { type: ["string", "null"], minLength: 5 }Universal Keywords
These work with any type and are not locked to a specific type builder:
const schema = new SchemaBuilder()
.$schema("https://json-schema.org/draft/2020-12/schema")
.$id("https://json-schema.org/draft/2020-12/schema")
.title("User Email")
.description("The user's primary email address")
.default("[email protected]")
.examples("[email protected]", "[email protected]")
.enum(["[email protected]", "[email protected]", "[email protected]"])
.readOnly()
.build();
// Result:
{
$schema: "https://json-schema.org/draft/2020-12/schema",
$id: "https://json-schema.org/draft/2020-12/schema"
title: "User Email",
description: "The user's primary email address",
default: "[email protected]",
examples: ["[email protected]", "[email protected]"],
enum: ["[email protected]", "[email protected]", "[email protected]"],
readOnly: true
}Universal keywords include:
| Keyword | Description |
| -------------------------------- | ------------------------------------- |
| .$schema(schema) | Schema draft |
| .$id(schemaId) | Unique schema identifier |
| .$ref(ref) | Schema reference |
| .$dynamicRef(dynamicRef) | Dynamic schema reference |
| .$anchor(anchor) | Schema identifier |
| .$dynamicAnchor(dynamicAnchor) | Dynamic schema identifier |
| .definitions(definitions) | Schema definitions(draft 7 and below) |
| .$defs(defs) | Schema definitions |
| .title(title) | Human-readable title |
| .description(desc) | Human-readable description |
| .default(value) | Default value |
| .examples(...values) | Example values |
| .enum(values) | Array of values (any type) |
| .const(value) | Single allowed value (any type) |
| .readOnly() | Mark as read-only |
| .writeOnly() | Mark as write-only |
| .not(schema) | Negation |
| .anyOf(...schemas) | Match at least one |
| .allOf(...schemas) | Match all |
| .oneOf(...schemas) | Match exactly one |
| .if(condition) | Conditional validation |
| .errorMessage(msg) | Custom error messages |
| .option(key, value) | Any custom keyword |
These can be combined with any type:
// Enum with numbers
new SchemaBuilder().number().enum([1, 2, 3, 5, 8, 13]).build();
// Const with boolean
new SchemaBuilder().boolean().const(true).build();
// Composition without a type
new SchemaBuilder()
.anyOf(
(s) => s.string(),
(s) => s.number(),
)
.build();TypeScript Type Locking
When chaining type methods, the first type called determines which keywords TypeScript makes available:
// String-first: string keywords available
new SchemaBuilder()
.string() // Locks to StringSchemaBuilder
.number() // Adds "number" to type array and changes to NumberSchemaBuilder
.minLength(5) // ❌ TypeScript error - String keyword
.pattern(/^a/) // ❌ TypeScript error - String keyword
.minimum(0); // ✅ number keyword - available
// Number-first: number keywords available
new SchemaBuilder()
.number() // Locks to NumberSchemaBuilder
.string() // Adds "string" to type array, changed to StringSchemaBuilder
.minimum(0) // ❌ TypeScript error - Number keyword
.multipleOf(5) // ❌ TypeScript error - Number keyword
.minLength(5); // ✅ string keyword - available
// Correct Approach
new SchemaBuilder()
.number()
.minimum(0)
.multipleOf(5)
.string()
.minLength(5);Why this design?
This ensures strict typing, making sure only methods of the desired type are accessible. The last type called decides the available methods, but users can still accept other types if they want a multi-type schema with one primary type's constraints.
If you need to define constraints for multiple types, just call the new type after the constraints for existing type is fully defined :
const schema = new SchemaBuilder()
.string()
.minLength(5) // String constraint
.number() // Branches to NumberSchemaBuilder
.minimum(0) // Number constraint now available
.build();
// Result: { type: ["string", "number"], minLength: 5, minimum: 0 }String Schemas
Length Constraints
const usernameSchema = new SchemaBuilder()
.string()
.minLength(3)
.maxLength(20)
.build();
// { type: "string", minLength: 3, maxLength: 20 }Pattern Matching
// Using RegExp
const slugSchema = new SchemaBuilder()
.string()
.pattern(/^[a-z0-9-]+$/)
.build();
// Using string
const emailPattern = new SchemaBuilder()
.string()
.pattern("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$")
.build();Format Validation
const contactSchema = new SchemaBuilder()
.object()
.properties({
email: (s) => s.string().format("email"),
website: (s) => s.string().format("uri"),
createdAt: (s) => s.string().format("date-time"),
})
.build();
// Supported formats: email, uri, uri-reference, uri-template,
// date, time, date-time, duration, uuid, ipv4, ipv6, hostname,
// json-pointer, relative-json-pointer, regexNumber Schemas
Numeric Constraints
const priceSchema = new SchemaBuilder()
.number()
.minimum(0)
.maximum(1000000)
.build();
// { type: "number", minimum: 0, maximum: 1000000 }
// Exclusive bounds
const percentageSchema = new SchemaBuilder()
.number()
.exclusiveMinimum(0)
.exclusiveMaximum(100)
.build();
// { type: "number", exclusiveMinimum: 0, exclusiveMaximum: 100 }Multiple Of
const evenNumberSchema = new SchemaBuilder().integer().multipleOf(2).build();
const priceInCents = new SchemaBuilder().number().multipleOf(0.01).build();Convenience Methods
// Positive numbers (>= 0)
const positiveSchema = new SchemaBuilder().number().positive().build();
// { type: "number", minimum: 0 }
// Negative numbers (<= 0)
const negativeSchema = new SchemaBuilder().number().negative().build();
// { type: "number", maximum: 0 }
// Range shorthand
const scoreSchema = new SchemaBuilder().number().range(0, 100).build();
// { type: "number", minimum: 0, maximum: 100 }Enums & Constants
// Enum (multiple allowed values)
const statusSchema = new SchemaBuilder()
.string()
.enum(["active", "inactive", "pending"])
.build();
// Const (single allowed value)
const typeSchema = new SchemaBuilder().string().const("user").build();Array Schemas
Basic Arrays
// Array with item schema
const numbersSchema = new SchemaBuilder()
.array()
.items((s) => s.number())
.build();
// { type: "array", items: { type: "number" } }
// With size constraints
const tagsSchema = new SchemaBuilder()
.array()
.items((s) => s.string())
.minItems(1)
.maxItems(10)
.uniqueItems(true)
.build();Tuple Validation (Prefix Items)
// Fixed-length arrays with different types per position
const coordinateSchema = new SchemaBuilder()
.array()
.prefixItems(
s => s.number(), // latitude
s => s.number() // longitude
)
.minItems(2)
.maxItems(2)
.build();
// Result:
{
type: "array",
prefixItems: [
{ type: "number" },
{ type: "number" }
],
minItems: 2,
maxItems: 2
}Contains & Count Constraints
// Array must contain at least one number > 100
const arrayWithLargeNumber = new SchemaBuilder()
.array()
.items((s) => s.number())
.contains((s) => s.number().minimum(100))
.minContains(1)
.maxContains(5)
.build();Complex Array Items
const usersSchema = new SchemaBuilder()
.array()
.items((s) =>
s
.object()
.properties({
id: (s) => s.integer(),
name: (s) => s.string().minLength(1),
email: (s) => s.string().format("email"),
roles: (s) => s.array().items((s) => s.string()),
})
.required(["id", "name", "email"]),
)
.minItems(1)
.build();Unevaluated Items (Draft 2019-09+)
Control validation of array items not covered by prefixItems, items, or contains:
// Disallow any unevaluated items
const strictTuple = new SchemaBuilder()
.array()
.prefixItems(
s => s.string(),
s => s.number()
)
.unevaluatedItems(false)
.build();
// Result:
{
type: "array",
prefixItems: [
{ type: "string" },
{ type: "number" }
],
unevaluatedItems: false
}
// Valid: ["hello", 42]
// Invalid: ["hello", 42, "extra"]
// Allow unevaluated items with schema
const flexibleTuple = new SchemaBuilder()
.array()
.prefixItems(
s => s.string(),
s => s.number()
)
.unevaluatedItems(s => s.boolean())
.build();
// Valid: ["hello", 42, true, false]
// Invalid: ["hello", 42, "not a boolean"]
// Allow any unevaluated items
const openTuple = new SchemaBuilder()
.array()
.prefixItems(
s => s.string(),
s => s.number()
)
.unevaluatedItems(true)
.build();
// Valid: ["hello", 42, anything, anything, ...]Additional Items (Draft 07 and earlier)
For older drafts, use additionalItems with array-form items:
// Disallow additional items beyond the tuple
const strictTupleLegacy = new SchemaBuilder()
.array()
.items(
{ type: "string" },
{ type: "number" }
)
.additionalItems(false)
.build();
// Result:
{
type: "array",
items: [
{ type: "string" },
{ type: "number" }
],
additionalItems: false
}
// Valid: ["hello", 42]
// Invalid: ["hello", 42, "extra"]
// Allow additional items with schema
const flexibleTupleLegacy = new SchemaBuilder()
.array()
.items(
s => s.string(),
s => s.number()
)
.additionalItems(s => s.boolean())
.build();
// Valid: ["hello", 42, true, false]
// Invalid: ["hello", 42, "not a boolean"]
// Allow any additional items
const openTupleLegacy = new SchemaBuilder()
.array()
.items(
s => s.string(),
s => s.number()
)
.additionalItems(true)
.build();
// Valid: ["hello", 42, anything, ...]Note: In Draft 2020-12, array-form
itemswas replaced byprefixItems, andadditionalItemswas replaced byitems(schema form) for items beyond the tuple. UseunevaluatedItemsfor items not validated by any keyword.
tuple
prefixItemsanditemswas changed from.prefixItems([])and.items([])to.prefixItems(...[])and.items(...[])
Object Schemas
Properties And Required
// Basic object
const personSchema = new SchemaBuilder()
.object()
.properties({
firstName: (s) => s.string(),
lastName: (s) => s.string(),
age: (s) => s.integer().minimum(0),
})
.required(["firstName", "lastName"])
.build();Pattern Properties
// Properties matching a pattern
const configSchema = new SchemaBuilder()
.object()
.properties({
version: (s) => s.string(),
})
.patternProperties({
"^env_": (s) => s.string(), // env_* properties must be strings
"^flag_": (s) => s.boolean(), // flag_* properties must be booleans
})
.build();
// Matches: { version: "1.0", env_mode: "production", flag_debug: true }Additional Properties
// Disallow additional properties (strict)
const strictSchema = new SchemaBuilder()
.object()
.properties({
name: (s) => s.string(),
})
.additionalProperties(false)
.build();
// Allow additional properties with schema
const flexibleSchema = new SchemaBuilder()
.object()
.properties({
knownProp: (s) => s.string(),
})
.additionalProperties((s) => s.number())
.build();
// Any additional property must be a numberUnevaluated Properties (Draft 2019-09+)
Control validation of properties not evaluated by properties, patternProperties, additionalProperties, or any subschema in allOf, anyOf, oneOf, if/then/else:
// Disallow unevaluated properties
const strictComposition = new SchemaBuilder()
.object()
.allOf(
(s) =>
s.object().properties({
name: (s) => s.string(),
}),
(s) =>
s.object().properties({
age: (s) => s.integer(),
}),
)
.unevaluatedProperties(false)
.build();
// Valid: { name: "Alice", age: 30 }
// Invalid: { name: "Alice", age: 30, extra: "oops" }
// Allow unevaluated properties with schema
const flexibleComposition = new SchemaBuilder()
.object()
.allOf((s) =>
s.object().properties({
id: (s) => s.integer(),
}),
)
.unevaluatedProperties((s) => s.string())
.build();
// Valid: { id: 1, name: "Alice", description: "A user" }
// Invalid: { id: 1, count: 42 } // count is number, not string
// Allow any unevaluated properties
const openComposition = new SchemaBuilder()
.object()
.allOf((s) =>
s.object().properties({
id: (s) => s.integer(),
}),
)
.unevaluatedProperties(true)
.build();
// Valid: { id: 1, anything: "goes" }Property Name Constraints
// Property names must match a pattern
const schema = new SchemaBuilder()
.object()
.propertyNames((s) => s.string().pattern("^[a-z_]+$"))
.build();
// All property names must be lowercase with underscoresProperty Count
const schema = new SchemaBuilder()
.object()
.minProperties(1)
.maxProperties(10)
.build();Dependent Schemas
// When 'creditCard' exists, require 'billingAddress'
const paymentSchema = new SchemaBuilder()
.object()
.properties({
creditCard: (s) => s.string(),
billingAddress: (s) => s.string(),
shippingAddress: (s) => s.string(),
})
.dependentSchemas({
creditCard: (s) => s.object().required(["billingAddress"]),
})
.build();Dependent Required
// When 'country' is present, 'state' must also be present
const addressSchema = new SchemaBuilder()
.object()
.properties({
street: (s) => s.string(),
city: (s) => s.string(),
state: (s) => s.string(),
country: (s) => s.string(),
})
.dependentRequired({
country: ["state"],
state: ["city"],
})
.build();Dependencies
// When 'creditCard' exists, require 'billingAddress'
const paymentSchema = new SchemaBuilder()
.object()
.properties({
creditCard: (s) => s.string(),
billingAddress: (s) => s.string(),
shippingAddress: (s) => s.string(),
street: (s) => s.string(),
city: (s) => s.string(),
state: (s) => s.string(),
country: (s) => s.string(),
})
.dependencies({
creditCard: (s) => s.object().required(["billingAddress"]),
country: ["state"],
state: ["city"],
})
.build();Nested Objects
const companySchema = new SchemaBuilder()
.object()
.properties({
name: (s) => s.string(),
address: (s) =>
s
.object()
.properties({
street: (s) => s.string(),
city: (s) => s.string(),
country: (s) => s.string(),
})
.required(["street", "city", "country"]),
employees: (s) =>
s.array().items((s) =>
s.object().properties({
name: (s) => s.string(),
role: (s) => s.string(),
}),
),
})
.required(["name", "address"])
.build();Removing Properties
When extending schemas, remove unwanted fields:
const fullSchema = new SchemaBuilder()
.object()
.properties({
id: (s) => s.integer(),
name: (s) => s.string(),
password: (s) => s.string(),
secret: (s) => s.string(),
})
.required(["id", "name", "password"]);
const publicSchema = fullSchema
.extend()
.remove(
["password", "secret"], // Fields to remove
["properties", "required"], // Remove from both places
)
.build();
// Available targets: "properties", "required", "patternProperties",
// "dependencies", "dependentRequired"Making Fields Optional
Remove all required constraints:
const strictSchema = new SchemaBuilder()
.object()
.properties({ name: (s) => s.string() })
.required(["name"]);
const looseSchema = strictSchema
.extend()
.optional() // Removes required array entirely
.build();Boolean Schema Values
Many keywords accept true, false, a schema object, or a builder callback. Boolean values have special meanings:
Object Keywords
// additionalProperties
.additionalProperties(false) // No extra properties allowed
.additionalProperties(true) // Any extra properties allowed (default)
.additionalProperties(s => s.string()) // Extra properties must be strings
// unevaluatedProperties
.unevaluatedProperties(false) // No unevaluated properties allowed
.unevaluatedProperties(true) // Any unevaluated properties allowed
.unevaluatedProperties(s => s.number()) // Unevaluated must be numbersArray Keywords
// items
.items(false) // Array must be empty
.items(true) // Any items allowed
.items(s => s.string()) // All items must be strings
// additionalItems (Draft 7 and earlier)
.additionalItems(false) // No additional items beyond prefixItems
.additionalItems(true) // Any additional items allowed
.additionalItems(s => s.number()) // Additional items must be numbers
// unevaluatedItems
.unevaluatedItems(false) // No unevaluated items allowed
.unevaluatedItems(true) // Any unevaluated items allowed
.unevaluatedItems(s => s.string()) // Unevaluated must be strings
// contains
.contains(true) // Array must have at least one item
.contains(false) // Always fails (array can't contain anything)
.contains(s => s.number().minimum(10)) // Must contain number >= 10Composition Keywords
// not
.not(true) // Always fails (not true = false)
.not(false) // Always passes (not false = true)
.not(s => s.string()) // Must not be a string
// In anyOf, allOf, oneOf
.anyOf(
true, // Always matches
s => s.string(),
{ type: "number" } // Plain JSON also works
)Basically in json schema, a boolean is a schema so any keyword that accepts a schema also accepts a boolean.
Composition
allOf - Schema Intersection
All schemas must validate (AND logic):
// Combine multiple schemas
const strictStringSchema = new SchemaBuilder()
.allOf(
(s) => s.string().minLength(5),
(s) => s.string().maxLength(10),
(s) => s.string().pattern("^[A-Z]"),
)
.build();
// Result: Must be 5-10 chars AND start with uppercase
// Valid: "Hello", "World"
// Invalid: "hi" (too short), "hello" (no uppercase start)Practical Example - Combining Base + Extension:
const baseUserSchema = new SchemaBuilder()
.object()
.properties({
id: (s) => s.integer(),
name: (s) => s.string(),
})
.required(["id", "name"]);
const adminUserSchema = new SchemaBuilder()
.allOf(
(s) => s.extend(baseUserSchema), // Include all base properties
(s) =>
s
.object()
.properties({
permissions: (s) => s.array().items((s) => s.string()),
adminLevel: (s) => s.integer().minimum(1),
})
.required(["permissions"]),
)
.build();
// Result: Admin has id, name, permissions, adminLevelanyOf - Schema Union
At least one schema must validate (OR logic):
// Accept string OR number
const flexibleIdSchema = new SchemaBuilder()
.anyOf(
(s) => s.string().pattern("^[A-Z]{3}\\d{3}$"), // e.g., "ABC123"
(s) => s.integer().minimum(1000), // e.g., 1234
)
.build();
// Valid: "ABC123" or 5000
// Invalid: "abc123" (lowercase), 500 (too small)Practical Example - Payment Methods:
const paymentMethodSchema = new SchemaBuilder()
.object()
.properties({
type: (s) => s.string(),
})
.anyOf(
// Credit card payment
(s) =>
s
.object()
.properties({
type: (s) => s.const("credit_card"),
cardNumber: (s) => s.string(),
cvv: (s) => s.string(),
})
.required(["type", "cardNumber", "cvv"]),
// Bank transfer
(s) =>
s
.object()
.properties({
type: (s) => s.const("bank_transfer"),
accountNumber: (s) => s.string(),
routingNumber: (s) => s.string(),
})
.required(["type", "accountNumber", "routingNumber"]),
// PayPal
(s) =>
s
.object()
.properties({
type: (s) => s.const("paypal"),
email: (s) => s.string().format("email"),
})
.required(["type", "email"]),
)
.build();oneOf - Exclusive Schema Union
Exactly one schema must validate (XOR logic):
// Must be EITHER a short string OR a long string, not both
const stringLengthSchema = new SchemaBuilder()
.oneOf(
(s) => s.string().maxLength(10), // Short string
(s) => s.string().minLength(50), // Long string
)
.build();
// Valid: "hello" (short), "this is a very long string..." (long)
// Invalid: "medium length string" (matches neither or both)Practical Example - Discriminated Union:
const shapeSchema = new SchemaBuilder()
.oneOf(
// Circle
(s) =>
s
.object()
.properties({
type: (s) => s.const("circle"),
radius: (s) => s.number().minimum(0),
})
.required(["type", "radius"]),
// Rectangle
(s) =>
s
.object()
.properties({
type: (s) => s.const("rectangle"),
width: (s) => s.number().minimum(0),
height: (s) => s.number().minimum(0),
})
.required(["type", "width", "height"]),
// Triangle
(s) =>
s
.object()
.properties({
type: (s) => s.const("triangle"),
base: (s) => s.number().minimum(0),
height: (s) => s.number().minimum(0),
})
.required(["type", "base", "height"]),
)
.build();
// Valid: { type: "circle", radius: 5 }
// Invalid: { type: "circle", radius: 5, width: 10 } (matches multiple)not - Schema Negation
Schema must NOT validate:
// Not a string
const notStringSchema = new SchemaBuilder().not((s) => s.string()).build();
// Valid: 123, true, {}, []
// Invalid: "hello"Practical Example - Exclusion Pattern:
// Accept any string EXCEPT email addresses
const nonEmailStringSchema = new SchemaBuilder()
.string()
.not((s) => s.string().format("email"))
.build();
// Valid: "hello", "test123"
// Invalid: "[email protected]"Complex Composition
const advancedSchema = new SchemaBuilder()
.object()
.properties({
value: (s) => s.string(),
})
.allOf(
// Must have 'value' property
(s) => s.object().required(["value"]),
)
.anyOf(
// Either has 'type' property
(s) =>
s
.object()
.properties({
type: (s) => s.string(),
})
.required(["type"]),
// Or has 'category' property
(s) =>
s
.object()
.properties({
category: (s) => s.string(),
})
.required(["category"]),
)
.not(
// But NOT both at the same time
(s) => s.object().required(["type", "category"]),
)
.build();
// Valid: { value: "x", type: "a" }
// Valid: { value: "x", category: "b" }
// Invalid: { value: "x", type: "a", category: "b" } (has both)Conditionals
if/then/else - Basic Conditional Logic
// If type is 'user', then require 'username'
const schema = new SchemaBuilder()
.object()
.properties({
type: (s) => s.string(),
username: (s) => s.string(),
apiKey: (s) => s.string(),
})
.if((s) =>
s.object().properties({
type: (s) => s.const("user"),
}),
)
.then((s) => s.object().required(["username"]))
.else((s) => s.object().required(["apiKey"]))
.build();
// If type="user": must have username
// Otherwise: must have apiKeyelseIf - Multiple Conditions
The elseIf extension allows clean chaining without deep nesting:
const accountSchema = new SchemaBuilder()
.object()
.properties({
accountType: (s) => s.string(),
username: (s) => s.string(),
email: (s) => s.string(),
companyName: (s) => s.string(),
taxId: (s) => s.string(),
})
.required(["accountType"])
.if((s) =>
s.object().properties({
accountType: (s) => s.const("personal"),
}),
)
.then((s) => s.object().required(["username", "email"]))
.elseIf((s) =>
s.object().properties({
accountType: (s) => s.const("business"),
}),
)
.then((s) => s.object().required(["companyName", "taxId", "email"]))
.elseIf((s) =>
s.object().properties({
accountType: (s) => s.const("enterprise"),
}),
)
.then((s) => s.object().required(["companyName", "taxId"]))
.else((s) => s.object().required(["email"]))
.build();
// Generates {if: schema, then:schema, elseIf[{if: schema, then:schema}, {if: schema, then:schema}], else: schema}
// That structure is perfectly handled by jet-Validator during compilation.Without elseIf (standard JSON Schema):
// Standard approach - deeply nested
.if(...)
.then(...)
.else(s => s
.if(...)
.then(...)
.else(s => s
.if(...)
.then(...)
.else(...)
)
)
// Nested hell!Nested Conditionals
const advancedRulesSchema = new SchemaBuilder()
.object()
.properties({
country: (s) => s.string(),
state: (s) => s.string(),
zipCode: (s) => s.string(),
postalCode: (s) => s.string(),
})
.if((s) =>
s.object().properties({
country: (s) => s.const("US"),
}),
)
.then(
(s) =>
s
.object()
.required(["state", "zipCode"])
.if((s) =>
s.object().properties({
state: (s) => s.const("California"),
}),
)
.then((s) =>
s.object().properties({
zipCode: (s) => s.string().pattern("^9[0-6]\\d{3}$"),
}),
)
.end(), // Only call .end if you don't need else.
)
.elseIf((s) =>
s.object().properties({
country: (s) => s.const("UK"),
}),
)
.then((s) => s.object().required(["postalCode"]))
.end()
.build();If you are not ending your conditional with
.else()then always call the.end()method
Check Jet-Validator documentation for keywords that support $data
References
$ref - Schema References
Local References
const schema = new SchemaBuilder()
.$defs({
address: (s) =>
s.object().properties({
street: (s) => s.string(),
city: (s) => s.string(),
zipCode: (s) => s.string(),
}),
})
.object()
.properties({
billingAddress: (s) => s.$ref("#/$defs/address"),
shippingAddress: (s) => s.$ref("#/$defs/address"),
})
.build();External References
// Reference external schema by URL
const userSchema = new SchemaBuilder()
.object()
.properties({
profile: (s) => s.$ref("https://example.com/schemas/profile.json"),
})
.build();
// Reference with fragment
const schema = new SchemaBuilder()
.object()
.properties({
user: (s) => s.$ref("https://example.com/schemas/common.json#/$defs/user"),
})
.build();RefBuilder - Type-Safe References
The RefBuilder provides a fluent API for constructing JSON Pointer references:
import { RefBuilder } from "@jetio/schema-builder";
// Basic usage
const ref1 = new RefBuilder().$defs("user").properties("address").build();
// Result: "#/$defs/user/properties/address"
// Using in schema
const schema = new SchemaBuilder()
.object()
.properties({
mainUser: (s) => s.$ref((r) => r.$defs("user")),
altUser: (s) => s.$ref((r) => r.$defs("user").properties("email")),
})
.build();RefBuilder API:
// Definitions
.$defs("schemaName") // #/$defs/schemaName
.definitions("schemaName") // #/definitions/schemaName
// Properties
.properties("propName") // /properties/propName
.patternProperties("pattern") // /patternProperties/pattern
.additionalProperties() // /additionalProperties
.propertyNames() // /propertyNames
// Arrays
.items() // /items
.items(0) // /items/0
.prefixItems(0) // /prefixItems/0
.contains() // /contains
.additionalItems() // /additionalItems
// Composition
.allOf(0) // /allOf/0
.anyOf(1) // /anyOf/1
.oneOf(2) // /oneOf/2
.not() // /not
// Conditionals
.if() // /if
.then() // /then
.else() // /else
.elseIf(0) // /elseIf/0
// Dependencies
.dependentSchemas("prop") // /dependentSchemas/prop
// Anchors
.anchor("anchorName") // #anchorName
.dynamicAnchor("name") // #name
// Base URL
.base("https://example.com/schema") // Change base URL
// Custom segments
.segment("customPath") // /customPath
// Utilities
.reset() // Clear path back to base
.chain() // Return self (for external composition)
.extend() // Clone for branching
// Building
.build() // Returns the complete reference string
.toString() // Alias for build()Complex RefBuilder Example:
const complexSchema = new SchemaBuilder()
.$defs({
person: (s) =>
s.object().properties({
name: (s) => s.string(),
contacts: (s) =>
s.array().items((s) =>
s.object().properties({
type: (s) => s.string(),
value: (s) => s.string(),
}),
),
}),
})
.object()
.properties({
// Reference nested property
primaryContact: (s) =>
s.$ref((r) => r.$defs("person").properties("contacts").items()),
// Reference with external base
externalRef: (s) =>
s.$ref((r) =>
r
.base("https://example.com/schema")
.$defs("common")
.properties("metadata"),
),
})
.build();RefBuilder with Anchors:
// Using anchors
const schema = new SchemaBuilder()
.$id("https://example.com/schema")
.$defs({
user: (s) =>
s
.object()
.$anchor("userSchema")
.properties({
name: (s) => s.string(),
}),
})
.object()
.properties({
// Reference by anchor
admin: (s) => s.$ref((r) => r.anchor("userSchema")),
// Or by path
moderator: (s) => s.$ref((r) => r.$defs("user")),
})
.build();RefBuilder Extension:
// Reuse partial refs
const baseRef = new RefBuilder()
.base("https://api.example.com/schemas")
.$defs("common");
// Extend for different endpoints
const userRef = baseRef.extend().properties("user").build();
// "https://api.example.com/schemas#/$defs/common/properties/user"
const productRef = baseRef.extend().properties("product").build();
// "https://api.example.com/schemas#/$defs/common/properties/product"$dynamicRef - Dynamic References
const schema = new SchemaBuilder()
.$dynamicAnchor("meta")
.object()
.properties({
// Dynamic reference resolves at validation time
nested: (s) => s.$dynamicRef((r) => r.anchor("meta")),
})
.build();Anchors
Define anchors for internal references:
const schema = new SchemaBuilder()
.$id("https://example.com/schema")
.$defs({
user: (s) =>
s
.object()
.$anchor("simpleUser") // Reference as #simpleUser
.$dynamicAnchor("recursiveUser") // Dynamic reference
.properties({
name: (s) => s.string(),
}),
})
.object()
.properties({
user: (s) => s.$ref("#simpleUser"),
})
.build();Definitions
$defs - Schema Definitions
Define reusable schema components:
const schema = new SchemaBuilder()
.$defs({
// Define multiple schemas
emailString: (s) => s.string().format("email"),
phoneString: (s) => s.string().pattern("^\\+?[1-9]\\d{1,14}$"),
address: (s) =>
s
.object()
.properties({
street: (s) => s.string(),
city: (s) => s.string(),
zipCode: (s) => s.string(),
})
.required(["street", "city"]),
person: (s) =>
s.object().properties({
name: (s) => s.string(),
email: (s) => s.$ref("#/$defs/emailString"),
phone: (s) => s.$ref("#/$defs/phoneString"),
address: (s) => s.$ref("#/$defs/address"),
}),
})
.object()
.properties({
customer: (s) => s.$ref("#/$defs/person"),
billing: (s) => s.$ref("#/$defs/address"),
})
.build();definitions (Legacy)
For JSON Schema Draft 6/7 compatibility:
const draft7Schema = new SchemaBuilder()
.definitions({
user: (s) =>
s.object().properties({
id: (s) => s.integer(),
name: (s) => s.string(),
}),
})
.object()
.properties({
author: (s) => s.$ref("#/definitions/user"),
})
.build();Custom Error Messages
Add custom error messages to your schemas for better validation feedback:
String Form (All Errors)
Override all validation errors with a single message:
const schema = new SchemaBuilder()
.string()
.minLength(5)
.maxLength(20)
.pattern("^[a-z]+$")
.errorMessage("Username must be 5-20 lowercase letters")
.build();
// ANY validation failure returns: "Username must be 5-20 lowercase letters"Object Form (Per-Keyword)
Customize error messages for specific validation keywords:
const schema = new SchemaBuilder()
.string()
.minLength(5)
.maxLength(20)
.pattern("^[a-z]+$")
.errorMessage({
type: "Must be text",
minLength: "Too short - need at least 5 characters",
maxLength: "Too long - maximum 20 characters",
pattern: "Only lowercase letters allowed",
})
.build();Property-Level Error Messages
const schema = new SchemaBuilder()
.object()
.properties({
email: (s) => s.string().format("email"),
age: (s) => s.integer().minimum(18),
})
.errorMessage({
properties: {
email: "Please enter a valid email address",
age: "You must be at least 18 years old",
},
})
.build();Per-Keyword Property Errors
const schema = new SchemaBuilder()
.object()
.properties({
email: (s) => s.string().format("email").minLength(5),
password: (s) => s.string().minLength(8).pattern("^(?=.*[A-Z])"),
})
.errorMessage({
properties: {
email: {
format: "Invalid email format",
minLength: "Email too short",
},
password: {
minLength: "Password must be at least 8 characters",
pattern: "Password must contain an uppercase letter",
},
},
})
.build();Nested Schema Error Messages
const schema = new SchemaBuilder()
.object()
.properties({
user: (s) =>
s
.object()
.properties({
name: (s) => s.string().minLength(2),
email: (s) => s.string().format("email"),
})
.errorMessage({
properties: {
name: "Name must be at least 2 characters",
email: "Invalid email",
},
}),
})
.build();The _jetError Fallback
Use _jetError as a catch-all for keywords without explicit messages:
const schema = new SchemaBuilder()
.object()
.properties({
user: (s) =>
s.object().properties({
name: (s) => s.string(),
email: (s) => s.string(),
}),
})
.errorMessage({
properties: {
user: {
_jetError: "User validation failed", // Fallback
properties: {
name: "Invalid name",
email: "Invalid email",
},
},
},
})
.build();Read the jet-Validator docuemntation to properly understand how error messages works Error Handling.
Advanced Features
Schema Metadata
const schema = new SchemaBuilder()
.$id("https://example.com/schemas/user.json")
.$schema("https://json-schema.org/draft/2020-12/schema")
.title("User Schema")
.description("Represents a user in the system")
.object()
.properties({
id: (s) => s.integer(),
})
.build();Custom Keywords
Add any custom keyword:
const schema = new SchemaBuilder()
.object()
.properties({
status: (s) => s.string(),
})
.option("x-custom-validator", "myValidator")
.option("x-internal", true)
.build();
// Result includes: "x-custom-validator": "myValidator"$data References
Enable runtime data references:
const schema = new SchemaBuilder()
.object()
.properties({
minValue: (s) => s.number(),
maxValue: (s) => s.number(),
currentValue: (s) =>
s
.number()
.minimum({ $data: "1/minValue" })
.maximum({ $data: "1/maxValue" }),
})
.build();
// currentValue validated against runtime min/max valuesMultiple Schema IDs
const schema = new SchemaBuilder()
.$id("https://example.com/base")
.$defs({
subSchema: (s) =>
s
.$id("sub-schema.json") // Relative ID
.object()
.properties({
value: (s) => s.string(),
}),
})
.build();
// Resolves to: https://example.com/sub-schema.jsonSchema Reuse & Extension
extend() - Clone & Modify
// Create base schema
const baseUser = new SchemaBuilder()
.object()
.properties({
id: (s) => s.integer(),
name: (s) => s.string(),
email: (s) => s.string().format("email"),
})
.required(["id", "name"]);
// Extend base schema
const adminUser = new SchemaBuilder()
.extend(baseUser) // Clone the schema
.properties({
role: (s) => s.string().const("admin"),
permissions: (s) => s.array().items((s) => s.string()),
})
.required(["role", "permissions"])
.build();
// baseUser is unchanged
// adminUser has: id, name, email, role, permissionsImportant Notes on Extension
Always extend a schema before building on it. You can replace most keywords by redefining them after extending.
Keywords that MERGE (cannot be replaced):
properties- New properties are added to existing onespatternProperties- New patterns are added to existing onesrequired- New required fields are added to existing array$defs/definitions- New definitions are added to existing onesdependentSchemas- New dependent schemas are added to existing onesdependencies- New dependencies are added to existing ones
Keywords that REPLACE (override existing):
- All other keywords (
type,oneOf,anyOf,allOf,enum,const,items, etc.)
Modifying merged keywords:
To modify properties from merged keywords, use helper methods:
// Remove properties and update required
const schema = new SchemaBuilder()
.extend(baseSchema)
.remove(['fieldName'], ['properties', 'required'])
.build();
// Make all fields optional
const optionalSchema = new SchemaBuilder()
.extend(baseSchema)
.optional()
.build();
// Override a specific property by redefining it
const updatedSchema = new SchemaBuilder()
.extend(baseSchema)
.properties({
existingField: (s) => s.string() // Redefines the type for this field
})
.build();Example:
const base = new SchemaBuilder()
.object()
.properties({
id: (s) => s.number(),
name: (s) => s.string()
})
.required(['id', 'name'])
.build();
// This ADDS to properties and required (doesn't replace)
const extended = new SchemaBuilder()
.extend(base)
.properties({ email: (s) => s.string() })
.required(['email'])
.build();
// Result: properties = { id, name, email }, required = ['id', 'name', 'email']
// To remove a property:
const withoutName = new SchemaBuilder()
.extend(base)
.remove(['name'], ['properties'])
.build();
// Result: properties = { id }, required = ['id']
// To make all optional:
const allOptional = new SchemaBuilder()
.extend(base)
.optional()
.build();
// Result: properties = { id, name }, required = []See available targets for remove in objects above.
Loading External Schemas
From URL
const schema = await new SchemaBuilder()
.url("https://example.com/schemas/user.json")
.extend()
.properties({
extraField: (s) => s.string(),
})
.build();From File
const schema = await new SchemaBuilder()
.file("./schemas/base.json")
.extend()
.properties({
additionalProp: (s) => s.string(),
})
.build();From JSON
const existingSchema = {
type: "object",
properties: {
name: { type: "string" },
},
};
const extended = new SchemaBuilder()
.json(existingSchema)
.extend()
.properties({
age: (s) => s.number(),
})
.required(["name", "age"])
.build();Note: When loading an external schema with either file, url or json, ensure its done at the beginning of the schema builder as it replaces the entire schema object of that schema builder. Ensure its done first before building on the schema. This rule also applies to extend.
Composition with extend()
const timestampMixin = new SchemaBuilder().object().properties({
createdAt: (s) => s.string().format("date-time"),
updatedAt: (s) => s.string().format("date-time"),
});
const auditMixin = new SchemaBuilder().object().properties({
createdBy: (s) => s.string(),
modifiedBy: (s) => s.string(),
});
const fullSchema = new SchemaBuilder()
.object()
.properties({
id: (s) => s.integer(),
data: (s) => s.string(),
})
.allOf(
(s) => s.extend(timestampMixin),
(s) => s.extend(auditMixin),
)
.build();
// Result has: id, data, createdAt, updatedAt, createdBy, modifiedByMixed Approaches
You can freely mix JSON objects with builder syntax:
JSON in Properties
const schema = new SchemaBuilder()
.object()
.properties({
// Plain JSON
simpleField: { type: "string", minLength: 5 },
// Builder syntax
complexField: (s) =>
s.object().properties({
nested: (s) => s.number(),
}),
// Mix in same schema
mixed: {
type: "object",
properties: {
plain: { type: "string" },
},
},
})
.build();JSON in Definitions
const schema = new SchemaBuilder()
.$defs({
// Plain JSON
simpleType: { type: "string", format: "email" },
// Builder
complexType: (s) =>
s.object().properties({
value: (s) => s.number(),
}),
// Nested mix
mixedType: {
type: "object",
properties: {
items: {
type: "array",
items: { type: "string" },
},
},
},
})
.build();JSON in Composition
const schema = new SchemaBuilder()
.anyOf(
// Plain JSON
{ type: "string", minLength: 5 },
// Builder
(s) => s.number().minimum(0),
// Boolean
true,
// Mixed
{
type: "object",
properties: {
special: { type: "boolean" },
},
},
)
.build();When to Use Each
Use Builder When:
- Building schemas programmatically
- Need autocomplete/type safety
- Complex nested structures
- Reusing schema components
- Dynamic schema generation
Use JSON When:
- Copying from existing JSON Schema
- Performance-critical
- Interfacing with external systems
Mix When:
- Migrating existing schemas
- Some parts are static, others dynamic
- Balancing readability and flexibility
Complete Examples
E-Commerce Product Schema
const productSchema = new SchemaBuilder()
.$id("https://example.com/schemas/product.json")
.$defs({
price: (s) =>
s
.object()
.properties({
amount: (s) => s.number().minimum(0),
currency: (s) => s.string().pattern("^[A-Z]{3}$"),
})
.required(["amount", "currency"]),
dimensions: (s) =>
s
.object()
.properties({
length: (s) => s.number().minimum(0),
width: (s) => s.number().minimum(0),
height: (s) => s.number().minimum(0),
unit: (s) => s.string().enum(["cm", "in", "m"]),
})
.required(["length", "width", "height", "unit"]),
})
.object()
.properties({
id: (s) => s.string().pattern("^PROD-\\d{6}$"),
name: (s) => s.string().minLength(3).maxLength(100),
description: (s) => s.string().maxLength(1000),
category: (s) =>
s.string().enum(["electronics", "clothing", "food", "books"]),
price: (s) => s.$ref("#/$defs/price"),
stock: (s) => s.integer().minimum(0),
dimensions: (s) => s.$ref("#/$defs/dimensions"),
tags: (s) =>
s
.array()
.items((s) => s.string())
.minItems(1)
.maxItems(10)
.uniqueItems(true),
availability: (s) =>
s.string().enum(["in_stock", "out_of_stock", "preorder"]),
})
.required(["id", "name", "category", "price", "stock", "availability"])
.if((s) =>
s.object().properties({
category: (s) => s.const("electronics"),
}),
)
.then((s) =>
s
.object()
.properties({
warranty: (s) =>
s
.object()
.properties({
months: (s) => s.integer().minimum(0),
type: (s) => s.string().enum(["limited", "full"]),
})
.required(["months", "type"]),
})
.required(["warranty"]),
)
.elseIf((s) =>
s.object().properties({
category: (s) => s.const("clothing"),
}),
)
.then((s) =>
s
.object()
.properties({
sizes: (s) =>
s
.array()
.items((s) => s.string().enum(["XS", "S", "M", "L", "XL", "XXL"]))
.minItems(1),
colors: (s) =>
s
.array()
.items((s) => s.string())
.minItems(1),
})
.required(["sizes", "colors"]),
)
.build();API Configuration Schema
const apiConfigSchema = new SchemaBuilder()
.$defs({
endpoint: (s) =>
s
.object()
.properties({
url: (s) => s.string().format("uri"),
method: (s) =>
s.string().enum(["GET", "POST", "PUT", "DELETE", "PATCH"]),
timeout: (s) => s.integer().minimum(100).maximum(30000),
retries: (s) => s.integer().minimum(0).maximum(5),
})
.required(["url", "method"]),
auth: (s) =>
s
.object()
.properties({
type: (s) => s.string(),
})
.if((s) => s.object().properties({ type: (s) => s.const("bearer") }))
.then((s) =>
s
.object()
.properties({
token: (s) => s.string(),
})
.required(["token"]),
)
.elseIf((s) => s.object().properties({ type: (s) => s.const("basic") }))
.then((s) =>
s
.object()
.properties({
username: (s) => s.string(),
password: (s) => s.string(),
})
.required(["username", "password"]),
)
.elseIf((s) =>
s.object().properties({ type: (s) => s.const("apikey") }),
)
.then((s) =>
s
.object()
.properties({
key: (s) => s.string(),
header: (s) => s.string().default("X-API-Key"),
})
.required(["key"]),
)
.end()
.required(["type"]),
})
.object()
.properties({
service: (s) => s.string().minLength(1),
baseUrl: (s) => s.string().format("uri"),
auth: (s) => s.$ref("#/$defs/auth"),
endpoints: (s) =>
s
.object()
.patternProperties({
"^[a-zA-Z][a-zA-Z0-9_]*$": (s) => s.$ref("#/$defs/endpoint"),
})
.minProperties(1),
logging: (s) =>
s.object().properties({
enabled: (s) => s.boolean().default(true),
level: (s) => s.string().enum(["debug", "info", "warn", "error"]),
}),
})
.required(["service", "baseUrl", "auth", "endpoints"])
.build();Form Validation Schema
const formSchema = new SchemaBuilder()
.object()
.properties({
username: (s) =>
s
.string()
.minLength(3)
.maxLength(20)
.pattern("^[a-zA-Z0-9_]+$")
.title("Username")
.description("Alphanumeric characters and underscores only")
.errorMessage({
minLength: "Username must be at least 3 characters",
maxLength: "Username cannot exceed 20 characters",
pattern:
"Username can only contain letters, numbers, and underscores",
}),
email: (s) =>
s
.string()
.format("email")
.title("Email Address")
.errorMessage("Please enter a valid email address"),
password: (s) =>
s
.string()
.minLength(8)
.pattern("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)")
.title("Password")
.description("Must contain uppercase, lowercase, and number")
.errorMessage({
minLength: "Password must be at least 8 characters",
pattern: "Password must contain uppercase, lowercase, and a number",
}),
confirmPassword: (s) =>
s
.string()
.const({ $data: "1/password" })
.title("Confirm Password")
.errorMessage("Passwords do not match"),
age: (s) =>
s.integer().minimum(13).maximum(120).title("Age").errorMessage({
minimum: "You must be at least 13 years old",
maximum: "Please enter a valid age",
}),
country: (s) => s.string().enum(["US", "UK", "CA", "AU"]).title("Country"),
agreedToTerms: (s) =>
s
.boolean()
.const(true)
.title("I agree to the terms and conditions")
.errorMessage("You must agree to the terms and conditions"),
})
.required([
"username",
"email",
"password",
"confirmPassword",
"age",
"agreedToTerms",
])
.if((s) =>
s.object().properties({
country: (s) => s.const("US"),
}),
)
.then((s) =>
s
.object()
.properties({
state: (s) =>
s
.string()
.pattern("^[A-Z]{2}$")
.title("State")
.errorMessage("Please enter a valid 2-letter state code"),
})
.required(["state"]),
)
.build();Best Practices
1. Use Callbacks for Nested Schemas
// ✅ Good - clean and readable
.properties({
user: s => s.object().properties({
name: s => s.string()
})
})
// ❌ Avoid - harder to read
.properties({
user: new SchemaBuilder().object().properties(...)
})2. Extract Complex Schemas
// ✅ Good - reusable
const addressSchema = (s: SchemaBuilder) =>
s.object().properties({
street: (s) => s.string(),
city: (s) => s.string(),
});
const schema = new SchemaBuilder()
.object()
.properties({
billing: addressSchema,
shipping: addressSchema,
})
.build();3. Use $defs for Reusable Components
// ✅ Good - DRY principle
.$defs({
email: s => s.string().format("email")
})
.properties({
primary: s => s.$ref("#/$defs/email"),
secondary: s => s.$ref("#/$defs/email")
})4. Leverage RefBuilder for Complex References
// ✅ Good - type-safe
.$ref(r => r.$defs("user").properties("address"))
// ❌ Avoid - error-prone
.$ref("#/$defs/user/properties/address")5. Use extend() for Schema Composition
// ✅ Good - composable
const base = new SchemaBuilder().object().properties({...});
const extended = new SchemaBuilder().extend(base).properties({...}).build();6. Add Error Messages for User-Facing Validation
// ✅ Good - helpful error messages
.string()
.minLength(8)
.errorMessage("Password must be at least 8 characters")
// ❌ Avoid - cryptic default errors for end users
.string()
.minLength(8)Tips & Tricks
Chaining Multiple Types
// Creates type: ["string", "number", "null"]
new SchemaBuilder()
.null()
.number()
.string()
.minLength(5) // Only applied when value is string
.build();Building Incrementally
let schema = new SchemaBuilder().object();
if (requireAuth) {
schema = schema.properties({
token: (s) => s.string(),
});
}
if (requireAdmin) {
schema = schema.properties({
adminKey: (s) => s.string(),
});
}
const final = schema.build();Common Patterns
Discriminated Unions
const eventSchema = new SchemaBuilder()
.object()
.properties({
type: (s) => s.string(),
})
.required(["type"])
.oneOf(
(s) =>
s.object().properties({
type: (s) => s.const("click"),
x: (s) => s.number(),
y: (s) => s.number(),
}),
(s) =>
s.object().properties({
type: (s) => s.const("scroll"),
scrollY: (s) => s.number(),
}),
)
.build();Pagination
const paginatedSchema = (itemSchema: (s: SchemaBuilder) => SchemaBuilder) =>
new SchemaBuilder()
.object()
.properties({
items: (s) => s.array().items(itemSchema),
total: (s) => s.integer().minimum(0),
page: (s) => s.integer().minimum(1),
pageSize: (s) => s.integer().minimum(1).maximum(100),
})
.required(["items", "total", "page", "pageSize"])
.build();Polymorphic IDs
const idSchema = new SchemaBuilder()
.anyOf(
(s) => s.string().pattern("^[0-9a-f]{24}$"), // MongoDB ObjectId
(s) => s.string().format("uuid"), // UUID
(s) => s.integer().minimum(1), // Integer ID
)
.build();API Reference
SchemaBuilder Methods
Type Methods
| Method | Returns | Description |
| ------------ | --------------------- | --------------------- |
| .string() | StringSchemaBuilder | Set type to string |
| .number() | NumberSchemaBuilder | Set type to number |
| .integer() | NumberSchemaBuilder | Set type to integer |
| .boolean() | BooleanSchema | Set type to boolean |
| .null() | NullSchema | Set type to null |
| .array() | ArraySchemaBuilder | Set type to array |
| .object() | ObjectSchemaBuilder | Set type to object |
Metadata Methods
| Method | Parameters | Description |
| ------------------------- | ----------------- | ---------------------- |
| .$id(id) | string | Set $id (Draft 6+) |
| .$schema(uri) | string | Set $schema |
| .$anchor(name) | string | Set $anchor |
| .$dynamicAnchor(name) | string | Set $dynamicAnchor |
| .title(title) | string | Set title |
| .description(desc) | string | Set description |
| .default(value) | any | Set default value |
| .examples(...values) | any[] | Set examples |
| .option(key, value) | string, any | Set any custom keyword |
Value Constraint Methods
| Method | Parameters | Description |
| --------------- | --------------------- | ------------------------ |
| .enum(values) | any[] or $data | Set allowed values |
| .const(value) | any | Set single allowed value |
Composition Methods
| Method | Parameters | Description |
| -------------------- | ----------------------------------------------------------------- | ------------------------- |
| .not(schema) | BuilderSchema or (b: SchemaBuilder) => SchemaBuilder | Negate a schema |
| .anyOf(...schemas) | BuilderSchema[] or ((b: SchemaBuilder) => SchemaBuilder)[] | Match at least one schema |
| .allOf(...schemas) | BuilderSchema[] or ((b: SchemaBuilder) => SchemaBuilder)[] | Match all schemas |
| .oneOf(...schemas) | BuilderSchema[] or ((b: SchemaBuilder) => SchemaBuilder)[] | Match exactly one schema |
Note: BuilderSchema = SchemaDefinition | SchemaBuilder<any> | boolean
Conditional Methods
| Method | Parameters | Returns | Description |
| -------------------- | ---------------------------------------------------- | ------------------ | ----------------------------- |
| .if(condition) | BuilderSchema
