Rilaykit Logorilaykit ✨
Core concepts

Validation Adapters

Integrate popular validation libraries like Zod, Yup, and Joi with Rilay forms.

The @rilaykit/validation-adapters package provides seamless integration between popular schema validation libraries and Rilay's form system, allowing you to use your existing validation schemas.

Installation

# Install the adapters package
npm install @rilaykit/validation-adapters

# Install your preferred validation library
npm install zod
# Or for Yup
npm install yup
# Or for Joi  
npm install joi
# Install the adapters package
pnpm add @rilaykit/validation-adapters

# Install your preferred validation library
pnpm add zod
# Or for Yup
pnpm add yup
# Or for Joi
pnpm add joi
# Install the adapters package
yarn add @rilaykit/validation-adapters

# Install your preferred validation library
yarn add zod
# Or for Yup
yarn add yup
# Or for Joi
yarn add joi

Supported Libraries

LibraryStatusImport
Zod✅ Available@rilaykit/validation-adapters/zod
Yup✅ Available@rilaykit/validation-adapters/yup
Joi✅ Available@rilaykit/validation-adapters/joi

Zod Integration

Basic Field Validation

Use Zod schemas directly in your Rilay forms:

import { z } from 'zod';
import { createZodValidator } from '@rilaykit/validation-adapters/zod';

// Create Zod validators
const emailValidator = createZodValidator(
  z.string().email('Please enter a valid email address')
);

const ageValidator = createZodValidator(
  z.number().min(18, 'You must be at least 18 years old')
);

// Use in form configuration
const form = factory
  .form('user-registration')
  .add({
    id: 'email',
    type: 'email',
    props: { label: 'Email Address' },
    validation: {
      validators: [emailValidator]
    }
  })
  .add({
    id: 'age',
    type: 'number',
    props: { label: 'Age' },
    validation: {
      validators: [ageValidator]
    }
  });

Component-Level Validation

Apply Zod validation to all instances of a component:

const factory = ril
  .create()
  .addComponent('email', {
    renderer: EmailInput,
    validation: {
      validators: [
        createZodValidator(z.string().email('Invalid email format'))
      ]
    }
  })
  .addComponent('password', {
    renderer: PasswordInput,
    validation: {
      validators: [
        createZodValidator(
          z.string()
            .min(8, 'Password must be at least 8 characters')
            .regex(/[A-Z]/, 'Must contain at least one uppercase letter')
            .regex(/[0-9]/, 'Must contain at least one number')
        )
      ]
    }
  });

Form-Level Validation

Validate entire forms with complex cross-field validation:

import { createZodFormValidator } from '@rilaykit/validation-adapters/zod';

// Define complete form schema
const registrationSchema = z.object({
  email: z.string().email('Invalid email format'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
  confirmPassword: z.string(),
  terms: z.boolean().refine(val => val === true, 'You must accept the terms')
}).refine(data => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  path: ["confirmPassword"]
});

// Create form validator
const formValidator = createZodFormValidator(registrationSchema);

// Apply to entire form
const form = factory
  .form('registration')
  .add({ id: 'email', type: 'email' })
  .add({ id: 'password', type: 'password' })
  .add({ id: 'confirmPassword', type: 'password' })
  .add({ id: 'terms', type: 'checkbox' })
  .setValidation({
    validators: [formValidator]
  });

Advanced Features

Async Validation

Integrate async validation like API calls:

// Schema with async refinement
const usernameSchema = z.string()
  .min(3, 'Username must be at least 3 characters')
  .refine(async (username) => {
    const response = await fetch(`/api/check-username/${username}`);
    const { available } = await response.json();
    return available;
  }, 'Username is already taken');

// Create async validator
const asyncValidator = createZodValidator(usernameSchema, {
  parseMode: 'async'
});

// Use with debouncing for better UX
const form = factory
  .form()
  .add({
    id: 'username',
    type: 'text',
    validation: {
      validators: [asyncValidator],
      validateOnBlur: true,
      debounceMs: 500
    }
  });

Custom Error Transformation

Transform Zod errors to be more user-friendly:

import { zodErrorTransforms } from '@rilaykit/validation-adapters/zod';

const validator = createZodValidator(
  z.string().min(3).max(50),
  {
    // Use built-in transformation
    errorTransform: zodErrorTransforms.userFriendly,
    
    // Custom transformation
    errorTransform: (issues) => {
      const issue = issues[0];
      switch (issue.code) {
        case 'too_small':
          return 'This field is too short';
        case 'too_big':
          return 'This field is too long';
        default:
          return issue.message;
      }
    }
  }
);

Path Formatting

Customize how nested field paths are displayed:

import { zodPathFormatters } from '@rilaykit/validation-adapters/zod';

const validator = createZodValidator(
  z.object({
    user: z.object({
      profile: z.object({
        name: z.string().min(1)
      })
    })
  }),
  {
    pathFormatter: zodPathFormatters.humanReadable // "User Profile Name"
    // or zodPathFormatters.bracketNotation     // "user[profile][name]"
    // or zodPathFormatters.dotNotation         // "user.profile.name" (default)
  }
);

Validation Presets

Use predefined configurations for common scenarios:

import { createZodValidatorWithPreset } from '@rilaykit/validation-adapters/zod';

const validator = createZodValidatorWithPreset(
  z.string().email(),
  'userFriendly'  // Transforms errors to be more readable
);

// Equivalent to:
// createZodValidator(schema, {
//   parseMode: 'sync',
//   errorTransform: zodErrorTransforms.concatenated,
//   pathFormatter: zodPathFormatters.humanReadable,
//   abortEarly: false
// })
const validator = createZodValidatorWithPreset(
  z.string().email(),
  'fast'  // Aborts on first error for better performance
);

// Equivalent to:
// createZodValidator(schema, {
//   parseMode: 'sync',
//   errorTransform: zodErrorTransforms.firstOnly,
//   abortEarly: true
// })
const validator = createZodValidatorWithPreset(
  z.string().email(),
  'strict'  // Uses original Zod errors without transformation
);

// Equivalent to:
// createZodValidator(schema, {
//   parseMode: 'sync',
//   abortEarly: false
// })
const validator = createZodValidatorWithPreset(
  z.string().email(),
  'async'  // Optimized for async validation
);

// Equivalent to:
// createZodValidator(schema, {
//   parseMode: 'async',
//   errorTransform: zodErrorTransforms.userFriendly,
//   pathFormatter: zodPathFormatters.dotNotation,
//   abortEarly: false
// })

Real-World Example

Here's a complete example of a complex registration form using Zod validation:

import { z } from 'zod';
import { 
  createZodValidator, 
  createZodFormValidator,
  zodErrorTransforms 
} from '@rilaykit/validation-adapters/zod';
import { ril, when, required } from '@rilaykit/core';

// Define schemas
const personalSchema = z.object({
  firstName: z.string().min(2, 'First name must be at least 2 characters'),
  lastName: z.string().min(2, 'Last name must be at least 2 characters'),
  email: z.string().email('Please enter a valid email address'),
  phone: z.string().regex(/^\d{3}-\d{3}-\d{4}$/, 'Phone must be XXX-XXX-XXXX')
});

const businessSchema = z.object({
  userType: z.enum(['individual', 'business']),
  companyName: z.string().optional(),
  taxId: z.string().optional()
}).refine(data => {
  if (data.userType === 'business') {
    return data.companyName && data.companyName.length > 0;
  }
  return true;
}, {
  message: 'Company name is required for business accounts',
  path: ['companyName']
});

// Create validators
const emailValidator = createZodValidator(
  personalSchema.shape.email,
  { errorTransform: zodErrorTransforms.userFriendly }
);

const phoneValidator = createZodValidator(
  personalSchema.shape.phone,
  { abortEarly: true }
);

const formValidator = createZodFormValidator(
  personalSchema.merge(businessSchema)
);

// Configure factory
const factory = ril
  .create()
  .addComponent('text', { renderer: TextInput })
  .addComponent('email', { renderer: EmailInput })
  .addComponent('select', { renderer: SelectInput });

// Build form
const registrationForm = factory
  .form('registration')
  .add({
    id: 'userType',
    type: 'select',
    props: {
      label: 'Account Type',
      options: [
        { value: 'individual', label: 'Individual' },
        { value: 'business', label: 'Business' }
      ]
    }
  })
  .add(
    {
      id: 'firstName',
      type: 'text',
      props: { label: 'First Name' },
      validation: {
        validators: [createZodValidator(personalSchema.shape.firstName)]
      }
    },
    {
      id: 'lastName', 
      type: 'text',
      props: { label: 'Last Name' },
      validation: {
        validators: [createZodValidator(personalSchema.shape.lastName)]
      }
    }
  )
  .add({
    id: 'email',
    type: 'email',
    props: { label: 'Email Address' },
    validation: {
      validators: [
        emailValidator,
        // Mix Zod with built-in validators
        required('Email is required')
      ],
      validateOnBlur: true
    }
  })
  .add({
    id: 'phone',
    type: 'text',
    props: { label: 'Phone Number', placeholder: '123-456-7890' },
    validation: {
      validators: [phoneValidator],
      validateOnChange: true,
      debounceMs: 300
    }
  })
  .add({
    id: 'companyName',
    type: 'text',
    props: { label: 'Company Name' },
    conditions: {
      visible: when('userType').equals('business'),
      required: when('userType').equals('business')
    }
  })
  .add({
    id: 'taxId',
    type: 'text', 
    props: { label: 'Tax ID' },
    conditions: {
      visible: when('userType').equals('business'),
      required: when('userType').equals('business')
    }
  })
  .setValidation({
    validators: [formValidator]
  });

Integration with Workflows

Zod validators work seamlessly with Rilay workflows:

const onboardingWorkflow = factory
  .flow('user-onboarding')
  .addStep({
    id: 'personal-info',
    title: 'Personal Information',
    formConfig: factory.form()
      .add({
        id: 'email',
        type: 'email',
        validation: {
          validators: [
            createZodValidator(z.string().email()),
            // Mix with conditions
            required('Email is required')
          ]
        }
      })
  })
  .addStep({
    id: 'verification',
    title: 'Email Verification',
    conditions: {
      visible: when('personal-info.email').isNotEmpty()
    },
    formConfig: factory.form()
      .add({
        id: 'verificationCode',
        type: 'text',
        validation: {
          validators: [
            createZodValidator(
              z.string().length(6, 'Verification code must be 6 digits')
            )
          ]
        }
      })
  });

Yup Integration

Basic Field Validation

Use Yup schemas in your Rilay forms:

import * as yup from 'yup';
import { createYupValidator } from '@rilaykit/validation-adapters/yup';

// Create Yup validators
const emailValidator = createYupValidator(
  yup.string()
    .email('Please enter a valid email address')
    .required('Email is required')
);

const ageValidator = createYupValidator(
  yup.number()
    .min(18, 'You must be at least 18 years old')
    .required('Age is required')
);

// Use in form configuration
const form = factory
  .form('user-registration')
  .add({
    id: 'email',
    type: 'email',
    props: { label: 'Email Address' },
    validation: {
      validators: [emailValidator]
    }
  })
  .add({
    id: 'age',
    type: 'number',
    props: { label: 'Age' },
    validation: {
      validators: [ageValidator]
    }
  });

Form-Level Validation

Validate entire forms with cross-field validation:

import { createYupFormValidator } from '@rilaykit/validation-adapters/yup';

// Define complete form schema
const registrationSchema = yup.object({
  email: yup.string().email('Invalid email format').required('Email is required'),
  password: yup.string().min(8, 'Password must be at least 8 characters').required(),
  confirmPassword: yup.string()
    .required('Confirm password is required')
    .oneOf([yup.ref('password')], 'Passwords must match')
});

// Create form validator
const formValidator = createYupFormValidator(registrationSchema);

// Apply to entire form
const form = factory
  .form('registration')
  .add({ id: 'email', type: 'email' })
  .add({ id: 'password', type: 'password' })
  .add({ id: 'confirmPassword', type: 'password' })
  .setValidation({
    validators: [formValidator]
  });

Joi Integration

Basic Field Validation

Use Joi schemas in your Rilay forms:

import Joi from 'joi';
import { createJoiValidator } from '@rilaykit/validation-adapters/joi';

// Create Joi validators
const emailValidator = createJoiValidator(
  Joi.string()
    .email({ tlds: { allow: false } })
    .required()
    .messages({
      'string.email': 'Please enter a valid email address',
      'any.required': 'Email is required'
    })
);

const ageValidator = createJoiValidator(
  Joi.number()
    .min(18)
    .required()
    .messages({
      'number.min': 'You must be at least 18 years old',
      'any.required': 'Age is required'
    })
);

// Use in form configuration
const form = factory
  .form('user-registration')
  .add({
    id: 'email',
    type: 'email',
    props: { label: 'Email Address' },
    validation: {
      validators: [emailValidator]
    }
  })
  .add({
    id: 'age',
    type: 'number',
    props: { label: 'Age' },
    validation: {
      validators: [ageValidator]
    }
  });

Form-Level Validation

Validate entire forms with complex cross-field validation:

import { createJoiFormValidator } from '@rilaykit/validation-adapters/joi';

// Define complete form schema
const registrationSchema = Joi.object({
  email: Joi.string().email().required().messages({
    'string.email': 'Invalid email format',
    'any.required': 'Email is required'
  }),
  password: Joi.string().min(8).required().messages({
    'string.min': 'Password must be at least 8 characters',
    'any.required': 'Password is required'
  }),
  confirmPassword: Joi.string().required().messages({
    'any.required': 'Confirm password is required'
  })
}).custom((value, helpers) => {
  if (value.password !== value.confirmPassword) {
    return helpers.error('any.custom', { 
      path: ['confirmPassword'],
      message: 'Passwords must match'
    });
  }
  return value;
});

// Create form validator
const formValidator = createJoiFormValidator(registrationSchema);

// Apply to entire form
const form = factory
  .form('registration')
  .add({ id: 'email', type: 'email' })
  .add({ id: 'password', type: 'password' })
  .add({ id: 'confirmPassword', type: 'password' })
  .setValidation({
    validators: [formValidator]
  });

Advanced Features

Custom Error Transformation

import { zodErrorTransforms } from '@rilaykit/validation-adapters/zod';

const validator = createZodValidator(
  z.string().min(3).max(50),
  {
    // Use built-in transformation
    errorTransform: zodErrorTransforms.userFriendly,
    
    // Custom transformation
    errorTransform: (issues) => {
      const issue = issues[0];
      switch (issue.code) {
        case 'too_small':
          return 'This field is too short';
        case 'too_big':
          return 'This field is too long';
        default:
          return issue.message;
      }
    }
  }
);
import { yupErrorTransforms } from '@rilaykit/validation-adapters/yup';

const validator = createYupValidator(
  yup.string().min(3).max(50),
  {
    // Use built-in transformation
    errorTransform: yupErrorTransforms.userFriendly,
    
    // Custom transformation
    errorTransform: (errors) => {
      const error = errors[0];
      switch (error.type) {
        case 'min':
          return 'This field is too short';
        case 'max':
          return 'This field is too long';
        default:
          return error.message;
      }
    }
  }
);
import { joiErrorTransforms } from '@rilaykit/validation-adapters/joi';

const validator = createJoiValidator(
  Joi.string().min(3).max(50),
  {
    // Use built-in transformation
    errorTransform: joiErrorTransforms.userFriendly,
    
    // Custom transformation
    errorTransform: (details) => {
      const detail = details[0];
      switch (detail.type) {
        case 'string.min':
          return 'This field is too short';
        case 'string.max':
          return 'This field is too long';
        default:
          return detail.message;
      }
    }
  }
);

Validation Presets

Use predefined configurations for common scenarios:

import { createZodValidatorWithPreset } from '@rilaykit/validation-adapters/zod';

// User-friendly preset
const validator = createZodValidatorWithPreset(
  z.string().email(),
  'userFriendly'
);

// Fast preset (aborts on first error)
const fastValidator = createZodValidatorWithPreset(
  z.string().email(),
  'fast'
);

// Async preset
const asyncValidator = createZodValidatorWithPreset(
  z.string().email(),
  'async'
);
import { createYupValidatorWithPreset } from '@rilaykit/validation-adapters/yup';

// User-friendly preset
const validator = createYupValidatorWithPreset(
  yup.string().email(),
  'userFriendly'
);

// Fast preset (aborts on first error)
const fastValidator = createYupValidatorWithPreset(
  yup.string().email(),
  'fast'
);

// Lenient preset (strips unknown keys)
const lenientValidator = createYupValidatorWithPreset(
  yup.string().email(),
  'lenient'
);
import { createJoiValidatorWithPreset } from '@rilaykit/validation-adapters/joi';

// User-friendly preset
const validator = createJoiValidatorWithPreset(
  Joi.string().email(),
  'userFriendly'
);

// Fast preset (aborts on first error)
const fastValidator = createJoiValidatorWithPreset(
  Joi.string().email(),
  'fast'
);

// Lenient preset (allows unknown keys)
const lenientValidator = createJoiValidatorWithPreset(
  Joi.string().email(),
  'lenient'
);

Path Formatting

Customize how nested field paths are displayed:

import { zodPathFormatters } from '@rilaykit/validation-adapters/zod';

const validator = createZodValidator(
  z.object({
    user: z.object({
      profile: z.object({
        name: z.string().min(1)
      })
    })
  }),
  {
    pathFormatter: zodPathFormatters.humanReadable // "User Profile Name"
    // or zodPathFormatters.bracketNotation     // "user[profile][name]"
    // or zodPathFormatters.dotNotation         // "user.profile.name" (default)
  }
);
import { yupPathFormatters } from '@rilaykit/validation-adapters/yup';

const validator = createYupValidator(
  yup.object({
    user: yup.object({
      profile: yup.object({
        name: yup.string().required()
      })
    })
  }),
  {
    pathFormatter: yupPathFormatters.humanReadable // "User Profile Name"
    // or yupPathFormatters.bracketNotation     // "user[profile][name]"
    // or yupPathFormatters.dotNotation         // "user.profile.name" (default)
  }
);
import { joiPathFormatters } from '@rilaykit/validation-adapters/joi';

const validator = createJoiValidator(
  Joi.object({
    user: Joi.object({
      profile: Joi.object({
        name: Joi.string().required()
      })
    })
  }),
  {
    pathFormatter: joiPathFormatters.humanReadable // "User Profile Name"
    // or joiPathFormatters.bracketNotation     // "user[profile][name]"
    // or joiPathFormatters.dotNotation         // "user.profile.name" (default)
  }
);

Convenience Functions

Each adapter provides convenience functions for common use cases:

// Sync and async validators
const syncValidator = createZodSyncValidator(schema);
const asyncValidator = createZodAsyncValidator(schema);

// Form validators
const formValidator = createZodFormValidator(schema);
const strictFormValidator = createZodSyncFormValidator(schema);
const asyncFormValidator = createZodAsyncFormValidator(schema);
// Strict and lenient validators
const strictValidator = createYupStrictValidator(schema);
const lenientValidator = createYupLenientValidator(schema);

// Form validators
const formValidator = createYupFormValidator(schema);
const strictFormValidator = createYupStrictFormValidator(schema);
const lenientFormValidator = createYupLenientFormValidator(schema);
// Strict and lenient validators
const strictValidator = createJoiStrictValidator(schema);
const lenientValidator = createJoiLenientValidator(schema);

// Form validators
const formValidator = createJoiFormValidator(schema);
const strictFormValidator = createJoiStrictFormValidator(schema);
const lenientFormValidator = createJoiLenientFormValidator(schema);

API Reference

Field Validators

// Primary functions
createZodValidator<T>(schema: ZodSchema<T>, options?: ZodValidatorOptions): FieldValidator<T>
createZodSyncValidator<T>(schema: ZodSchema<T>, options?: Omit<ZodValidatorOptions, 'parseMode'>): FieldValidator<T>
createZodAsyncValidator<T>(schema: ZodSchema<T>, options?: Omit<ZodValidatorOptions, 'parseMode'>): FieldValidator<T>

// Preset helpers
createZodValidatorWithPreset<T>(schema: ZodSchema<T>, preset: PresetName, overrides?: Partial<ZodValidatorOptions>): FieldValidator<T>
// Primary functions
createYupValidator<T>(schema: yup.Schema<T>, options?: YupValidatorOptions): FieldValidator<T>
createYupStrictValidator<T>(schema: yup.Schema<T>, options?: Omit<YupValidatorOptions, 'stripUnknown'>): FieldValidator<T>
createYupLenientValidator<T>(schema: yup.Schema<T>, options?: Omit<YupValidatorOptions, 'stripUnknown'>): FieldValidator<T>

// Preset helpers
createYupValidatorWithPreset<T>(schema: yup.Schema<T>, preset: PresetName, overrides?: Partial<YupValidatorOptions>): FieldValidator<T>
// Primary functions
createJoiValidator<T>(schema: Joi.Schema, options?: JoiValidatorOptions): FieldValidator<T>
createJoiStrictValidator<T>(schema: Joi.Schema, options?: Omit<JoiValidatorOptions, 'allowUnknown'>): FieldValidator<T>
createJoiLenientValidator<T>(schema: Joi.Schema, options?: Omit<JoiValidatorOptions, 'allowUnknown'>): FieldValidator<T>

// Preset helpers
createJoiValidatorWithPreset<T>(schema: Joi.Schema, preset: PresetName, overrides?: Partial<JoiValidatorOptions>): FieldValidator<T>

Form Validators

createZodFormValidator<T>(schema: ZodSchema<T>, options?: ZodValidatorOptions): FormValidator<T>
createZodFormValidatorWithFieldErrors<T>(schema: ZodSchema<T>, options?: ZodValidatorOptions): FieldErrorsValidator<T>
createZodSyncFormValidator<T>(schema: ZodSchema<T>, options?: Omit<ZodValidatorOptions, 'parseMode'>): FormValidator<T>
createZodAsyncFormValidator<T>(schema: ZodSchema<T>, options?: Omit<ZodValidatorOptions, 'parseMode'>): FormValidator<T>
createYupFormValidator<T>(schema: yup.Schema<T>, options?: YupValidatorOptions): FormValidator<T>
createYupFormValidatorWithFieldErrors<T>(schema: yup.Schema<T>, options?: YupValidatorOptions): FieldErrorsValidator<T>
createYupStrictFormValidator<T>(schema: yup.Schema<T>, options?: Omit<YupValidatorOptions, 'stripUnknown'>): FormValidator<T>
createYupLenientFormValidator<T>(schema: yup.Schema<T>, options?: Omit<YupValidatorOptions, 'stripUnknown'>): FormValidator<T>
createJoiFormValidator<T>(schema: Joi.Schema, options?: JoiValidatorOptions): FormValidator<T>
createJoiFormValidatorWithFieldErrors<T>(schema: Joi.Schema, options?: JoiValidatorOptions): FieldErrorsValidator<T>
createJoiStrictFormValidator<T>(schema: Joi.Schema, options?: Omit<JoiValidatorOptions, 'allowUnknown'>): FormValidator<T>
createJoiLenientFormValidator<T>(schema: Joi.Schema, options?: Omit<JoiValidatorOptions, 'allowUnknown'>): FormValidator<T>

Options

interface ZodValidatorOptions {
  parseMode?: 'sync' | 'async';
  errorTransform?: (issues: ZodIssue[]) => string;
  pathFormatter?: (path: (string | number)[]) => string;
  abortEarly?: boolean;
}
interface YupValidatorOptions {
  context?: Record<string, any>;
  stripUnknown?: boolean;
  errorTransform?: (errors: any[]) => string;
  pathFormatter?: (path: (string | number)[]) => string;
  abortEarly?: boolean;
}
interface JoiValidatorOptions {
  context?: Record<string, any>;
  allowUnknown?: boolean;
  errorTransform?: (details: any[]) => string;
  pathFormatter?: (path: (string | number)[]) => string;
  abortEarly?: boolean;  
}

Mixing Validators: You can combine validation adapters with Rilay's built-in validators in the same field's validators array. They will be executed in order.

Performance: Use the fast preset or abortEarly: true for forms with many validation rules to improve performance by stopping at the first error.

Library Installation: Remember to install the validation library you want to use (zod, yup, or joi) alongside the adapters package. The adapters are designed to work without hard dependencies.