Validation
Built-in validators and Standard Schema support for type-safe form validation.
RilayKit ships with a built-in validation engine that implements the Standard Schema specification. This means you can use RilayKit's own validators or any Standard Schema-compatible library (Zod 3.24+, Valibot 1.0+, ArkType 2.0+) interchangeably -- without adapters.
Every built-in validator returns a StandardSchemaV1 object, so it can be mixed freely with Zod, Valibot, ArkType, or any other compliant library in the same validate array.
Built-in Validators
All built-in validators are imported from @rilaykit/core. Each one accepts an optional custom error message and returns a Standard Schema object.
required(message?)
Validates that the value is non-empty. Catches empty strings, null, undefined, empty arrays, and empty objects.
import { required } from '@rilaykit/core';
required() // Default: 'This field is required'
required('Please fill this in') // Custom messageemail(message?)
Validates email format.
import { email } from '@rilaykit/core';
email() // Default: 'Please enter a valid email address'
email('A valid email is required') // Custom messageurl(message?)
Validates URL format using the native URL constructor.
import { url } from '@rilaykit/core';
url() // Default: 'Please enter a valid URL'
url('Must be a valid link') // Custom messageminLength(min, message?)
Validates minimum string length.
import { minLength } from '@rilaykit/core';
minLength(3) // Default: 'Must be at least 3 characters long'
minLength(8, 'Password is too short') // Custom messagemaxLength(max, message?)
Validates maximum string length.
import { maxLength } from '@rilaykit/core';
maxLength(100) // Default: 'Must be no more than 100 characters long'
maxLength(50, 'Keep it under 50 characters') // Custom messagepattern(regex, message?)
Validates against a regular expression.
import { pattern } from '@rilaykit/core';
pattern(/^[a-z0-9]+$/) // Default: 'Value does not match required pattern'
pattern(/^\d{5}$/, 'Must be a 5-digit zip code') // Custom messagenumber(message?)
Validates that the value is a valid number. Also coerces strings to numbers.
import { number } from '@rilaykit/core';
number() // Default: 'Must be a valid number'
number('Enter a numeric value') // Custom messagemin(minValue, message?)
Validates a minimum numeric value. Coerces strings to numbers before comparison.
import { min } from '@rilaykit/core';
min(0) // Default: 'Must be at least 0'
min(18, 'You must be 18 or older') // Custom messagemax(maxValue, message?)
Validates a maximum numeric value. Coerces strings to numbers before comparison.
import { max } from '@rilaykit/core';
max(100) // Default: 'Must be no more than 100'
max(999, 'Value cannot exceed 999') // Custom messagecustom<T>(fn, message?)
Creates a synchronous custom validator. The function receives the field value and must return true (valid) or false (invalid).
import { custom } from '@rilaykit/core';
custom<string>(
(value) => value.startsWith('SK_'),
'Must start with SK_'
)
// Default message: 'Validation failed'async<T>(fn, message?)
Creates an asynchronous custom validator. The function receives the field value and must return a Promise<boolean>.
import { async } from '@rilaykit/core';
async<string>(
async (value) => {
const res = await fetch(`/api/check-username?name=${value}`);
const { available } = await res.json();
return available;
},
'Username is already taken'
)
// Default message: 'Async validation failed'combine<T>(...schemas)
Combines multiple Standard Schema validators into a single schema. Runs them in sequence and accumulates all issues.
import { combine, required, minLength, email } from '@rilaykit/core';
const emailValidator = combine(
required('Email is required'),
email(),
minLength(5, 'Email seems too short')
);Standard Schema Support
The Standard Schema specification defines a common interface for validation libraries. Any library that implements StandardSchemaV1 works directly with RilayKit -- no wrappers, no adapters.
| Library | Version | Standard Schema Support |
|---|---|---|
| Zod | 3.24.0+ | Native support |
| Valibot | 1.0+ | Native support |
| ArkType | 2.0+ | Native support |
import { z } from 'zod';
const form = rilay.form('signup')
.add({
id: 'email',
type: 'input',
props: { label: 'Email' },
validation: {
validate: [z.string().email()],
},
});import * as v from 'valibot';
const form = rilay.form('signup')
.add({
id: 'email',
type: 'input',
props: { label: 'Email' },
validation: {
validate: [v.pipe(v.string(), v.email())],
},
});import { type } from 'arktype';
const form = rilay.form('signup')
.add({
id: 'email',
type: 'input',
props: { label: 'Email' },
validation: {
validate: [type('string.email')],
},
});Combining Validators
You can freely mix built-in validators and third-party schemas in the same validate array. They are executed in order and all issues are accumulated.
import { required, minLength } from '@rilaykit/core';
import { z } from 'zod';
const form = rilay.form('profile')
.add({
id: 'username',
type: 'input',
props: { label: 'Username' },
validation: {
validate: [
required(), // RilayKit built-in
minLength(3), // RilayKit built-in
z.string().regex(/^[a-z0-9]+$/), // Zod schema
],
},
});The validation property is always an object with a validate key -- never a bare array. Use { validate: [...] }, not [...].
Validation Configuration
Field-Level: FieldValidationConfig
Each field accepts an optional validation object that controls what, when, and how validation runs.
interface FieldValidationConfig<T = any> {
/** One or more Standard Schema validators */
validate?: StandardSchema<T> | StandardSchema<T>[];
/** Run validation every time the value changes (default: false) */
validateOnChange?: boolean;
/** Run validation when the field loses focus (default: false) */
validateOnBlur?: boolean;
/** Debounce delay in milliseconds before validation fires */
debounceMs?: number;
}Usage example:
import { required, email } from '@rilaykit/core';
const form = rilay.form('contact')
.add({
id: 'email',
type: 'input',
props: { label: 'Email' },
validation: {
validate: [required(), email()],
validateOnBlur: true,
debounceMs: 300,
},
})
.add({
id: 'name',
type: 'input',
props: { label: 'Name' },
validation: {
validate: [required()],
validateOnChange: true,
},
});Form-Level Validation
For cross-field validation (e.g. "confirm password must match password"), use FormValidationConfig via the setValidation() method on a form builder.
FormValidationConfig
interface FormValidationConfig<T> {
/** One or more Standard Schema validators applied to the entire form data */
validate?: StandardSchema<T> | StandardSchema<T>[];
/** Run form-level validation on submit (default: depends on config) */
validateOnSubmit?: boolean;
/** Run form-level validation on workflow step change */
validateOnStepChange?: boolean;
}Usage with setValidation():
import { z } from 'zod';
const passwordSchema = z.object({
password: z.string().min(8, 'Password too short'),
confirmPassword: z.string(),
}).refine(
(data) => data.password === data.confirmPassword,
{
message: "Passwords don't match",
path: ['confirmPassword'],
}
);
const form = rilay
.form('change-password')
.add({
id: 'password',
type: 'password',
props: { label: 'New Password' },
})
.add({
id: 'confirmPassword',
type: 'password',
props: { label: 'Confirm Password' },
})
.setValidation({
validate: passwordSchema,
});import { custom } from '@rilaykit/core';
const passwordMatch = custom(
(data: any) => data.password === data.confirmPassword,
"Passwords don't match"
);
const form = rilay
.form('change-password')
.add({
id: 'password',
type: 'password',
props: { label: 'New Password' },
})
.add({
id: 'confirmPassword',
type: 'password',
props: { label: 'Confirm Password' },
})
.setValidation({
validate: passwordMatch,
});Utility Functions
RilayKit exports two utility functions for advanced use cases.
isStandardSchema(value)
Type guard that checks whether a value implements the Standard Schema interface (~standard property with version 1, a vendor string, and a validate function).
import { isStandardSchema } from '@rilaykit/core';
isStandardSchema(required()); // true
isStandardSchema(z.string().email()); // true
isStandardSchema('not a schema'); // falsecombineSchemas(...schemas)
Combines multiple Standard Schema objects into a single schema. Unlike passing an array to validate, this produces one unified schema object -- useful when you need to pass a single schema somewhere that does not accept arrays.
import { combineSchemas, required, email } from '@rilaykit/core';
import { z } from 'zod';
const combinedEmailSchema = combineSchemas(
required('Email is required'),
email(),
z.string().min(5, 'Email too short')
);
// Use as a single schema
const form = rilay.form('contact')
.add({
id: 'email',
type: 'input',
props: { label: 'Email' },
validation: {
validate: combinedEmailSchema, // single schema, not an array
},
});The difference between combine() (from validators) and combineSchemas() (from utilities) is purely organizational -- they produce the same result. Use whichever import feels clearest in context.