Rilaykit Logorilaykit ✨
Forms

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 changes
  • validateOnBlur: 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:

LibraryVersionStandard Schema Support
Zod3.24.0+✅ Native support
Yup1.7.0+✅ Native support
Joi18.0.0+✅ Native support
Valibot1.0+✅ Native support
ArkType2.0+✅ Native support

RilayKit Built-in Validators

RilayKit provides Standard Schema compatible validators out of the box:

  • required(message?): Ensures the field is not empty
  • email(message?): Validates email format
  • url(message?): Validates URL format
  • minLength(min, message?): Minimum string length
  • maxLength(max, message?): Maximum string length
  • pattern(regex, message?): Regular expression validation
  • number(message?): Valid number check
  • min(value, message?): Minimum numeric value
  • max(value, message?): Maximum numeric value
  • custom(fn, message?): Custom synchronous validator
  • async(fn, message?): Custom asynchronous validator
  • combine(...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,
  });