Universal Validation with Standard Schema
Learn how to use validation with any Standard Schema compatible library.
RilayKit features a universal validation system built on Standard Schema, allowing you to use any Standard Schema compatible validation library directly - including Zod, Yup, Joi, Valibot, ArkType, and more.
Universal Validation API
RilayKit uses a single, unified validate property that accepts any Standard Schema compatible validation. This means you can use external libraries directly without adapters.
The validation property accepts:
validate: A Standard Schema compatible validator (single or array)validateOnChange: Validate the field whenever its value changesvalidateOnBlur: Validate the field when the user leaves it (on blur)debounceMs: Debounce validation by a specified number of milliseconds
import { z } from 'zod';
import { rilay } from '@/lib/rilay';
const registrationForm = rilay
.form('registration')
.add({
id: 'email',
type: 'email',
props: { label: 'Email Address' },
validation: {
validate: z.string().email('Please enter a valid email'),
validateOnBlur: true,
},
})
.add({
id: 'password',
type: 'password',
props: { label: 'Password' },
validation: {
validate: z.string().min(8, 'Password too short'),
validateOnChange: true,
},
});import { rilay } from '@/lib/rilay';
import { required, minLength, email } from '@rilaykit/core';
const registrationForm = rilay
.form('registration')
.add({
id: 'email',
type: 'email',
props: { label: 'Email Address' },
validation: {
validate: [required(), email('Please enter a valid email')],
validateOnBlur: true,
},
})
.add({
id: 'password',
type: 'password',
props: { label: 'Password' },
validation: {
validate: [required(), minLength(8)],
validateOnChange: true,
},
});import { z } from 'zod';
import { rilay } from '@/lib/rilay';
import { required } from '@rilaykit/core';
const registrationForm = rilay
.form('registration')
.add({
id: 'email',
type: 'email',
props: { label: 'Email Address' },
validation: {
validate: [
required('Email is required'), // RilayKit built-in
z.string().email('Invalid email format'), // Zod schema
z.string().min(5, 'Email too short'), // Another Zod rule
],
validateOnBlur: true,
},
});Standard Schema Compatible Libraries
RilayKit works with any validation library that implements the Standard Schema interface:
| Library | Version | Standard Schema Support |
|---|---|---|
| Zod | 3.24.0+ | ✅ Native support |
| Yup | 1.7.0+ | ✅ Native support |
| Joi | 18.0.0+ | ✅ Native support |
| Valibot | 1.0+ | ✅ Native support |
| ArkType | 2.0+ | ✅ Native support |
RilayKit Built-in Validators
RilayKit provides Standard Schema compatible validators out of the box:
required(message?): Ensures the field is not emptyemail(message?): Validates email formaturl(message?): Validates URL formatminLength(min, message?): Minimum string lengthmaxLength(max, message?): Maximum string lengthpattern(regex, message?): Regular expression validationnumber(message?): Valid number checkmin(value, message?): Minimum numeric valuemax(value, message?): Maximum numeric valuecustom(fn, message?): Custom synchronous validatorasync(fn, message?): Custom asynchronous validatorcombine(...validators): Combine multiple validators
All built-in validators implement Standard Schema and can be mixed with external libraries.
Using External Libraries
import { z } from 'zod';
const form = rilay.form('user')
.add({
id: 'email',
type: 'input',
validation: {
validate: z.string()
.email('Invalid email format')
.min(5, 'Email too short'),
},
})
.add({
id: 'age',
type: 'input',
validation: {
validate: z.string()
.refine(val => {
const num = parseInt(val);
return !isNaN(num) && num >= 18;
}, 'Must be 18 or older'),
},
});import * as yup from 'yup';
const form = rilay.form('user')
.add({
id: 'email',
type: 'input',
validation: {
validate: yup.string()
.email('Invalid email format')
.min(5, 'Email too short')
.required(),
},
});import Joi from 'joi';
const form = rilay.form('user')
.add({
id: 'email',
type: 'input',
validation: {
validate: Joi.string()
.email()
.min(5)
.required()
.messages({
'string.email': 'Invalid email format',
'string.min': 'Email too short',
}),
},
});Creating Custom Standard Schema Validators
You can create your own Standard Schema compatible validators:
import type { StandardSchemaV1 } from '@standard-schema/spec';
export function containsRilay(message = 'Value must contain "rilay"'): StandardSchemaV1<string> {
return {
'~standard': {
version: 1,
vendor: 'my-app',
validate: (value: unknown) => {
if (typeof value === 'string' && value.includes('rilay')) {
return { value };
}
return { issues: [{ message }] };
},
},
};
}
// Use it like any other validator
const form = rilay.form('custom')
.add({
id: 'username',
type: 'input',
validation: {
validate: [required(), containsRilay()]
}
});Form-Level Validation
For cross-field validation, use Standard Schema object schemas with the setValidation method:
import { z } from 'zod';
const userSchema = 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 changePasswordForm = rilay
.form('change-password')
.add({ type: 'password', id: 'password', props: { label: 'New Password' } })
.add({ type: 'password', id: 'confirmPassword', props: { label: 'Confirm Password' } })
.setValidation({
validate: userSchema, // Zod object schema for cross-field validation
});import * as yup from 'yup';
const userSchema = yup.object({
password: yup.string().min(8, 'Password too short').required(),
confirmPassword: yup.string()
.oneOf([yup.ref('password')], 'Passwords must match')
.required(),
});
const changePasswordForm = rilay
.form('change-password')
.add({ type: 'password', id: 'password', props: { label: 'New Password' } })
.add({ type: 'password', id: 'confirmPassword', props: { label: 'Confirm Password' } })
.setValidation({
validate: userSchema, // Yup object schema
});import { custom } from '@rilaykit/core';
const passwordMatchValidator = custom(
(formData: any) => formData.password === formData.confirmPassword,
"Passwords don't match"
);
const changePasswordForm = rilay
.form('change-password')
.add({ type: 'password', id: 'password', props: { label: 'New Password' } })
.add({ type: 'password', id: 'confirmPassword', props: { label: 'Confirm Password' } })
.setValidation({
validate: passwordMatchValidator,
});Advanced Validation Patterns
Async Validation
Standard Schema supports asynchronous validation out of the box:
import { z } from 'zod';
const emailField = {
id: 'email',
type: 'email',
props: { label: 'Email' },
validation: {
validate: z.string()
.email('Invalid email format')
.refine(async (email) => {
// API call to check if email is unique
const response = await fetch(`/api/check-email?email=${email}`);
const { isUnique } = await response.json();
return isUnique;
}, 'Email is already taken'),
validateOnBlur: true,
debounceMs: 500, // Debounce async validation
},
};Combining Multiple Libraries
Mix and match validation libraries as needed:
import { z } from 'zod';
import * as yup from 'yup';
import { required, email } from '@rilaykit/core';
const form = rilay.form('mixed')
.add({
id: 'email',
type: 'input',
validation: {
validate: [
required('Email is required'), // RilayKit
z.string().min(5, 'Too short'), // Zod
yup.string().max(100, 'Too long'), // Yup
email('Invalid format'), // RilayKit
]
}
});Conditional Validation
Use form-level schemas for complex conditional validation:
import { z } from 'zod';
const userSchema = z.object({
userType: z.enum(['personal', 'business']),
email: z.string().email(),
companyName: z.string().optional(),
}).refine(data => {
if (data.userType === 'business' && !data.companyName) {
return false;
}
return true;
}, {
message: 'Company name is required for business users',
path: ['companyName'],
});
const form = rilay.form('conditional')
.add({ id: 'userType', type: 'select', /* ... */ })
.add({ id: 'email', type: 'input', /* ... */ })
.add({ id: 'companyName', type: 'input', /* ... */ })
.setValidation({
validate: userSchema,
});