Advanced Yup: Patterns, Performance, and Best Practices
Yup is a powerful schema validation library for JavaScript and TypeScript, widely used in the React ecosystem (especially with Formik) and standalone Node.js applications. While basic usage—string(), number(), required()—is well-documented, real-world applications demand more sophisticated patterns: conditional validation, recursive structures, cross-field dependencies, and high-performance schemas. This tutorial dives into advanced Yup techniques that help you build robust, maintainable, and efficient validation layers.
What Is Advanced Yup?
At its core, Yup provides a declarative API to define validation schemas. Advanced usage refers to leveraging Yup's full feature set to handle complex business rules without sacrificing readability or performance. This includes:
- Conditional schemas with
.when()and custom conditions - Recursive schemas for nested or self-referencing data
- Cross-field validation using
.test()with access to sibling values - Schema composition and reuse through factories and mixins
- Performance optimizations like lazy evaluation, caching, and schema memoization
- Type inference from schemas for TypeScript users
Why Advanced Patterns Matter
Standard validation quickly breaks under real-world constraints. For example, an order form where shipping method determines required fields, a configuration file with circular references, or a signup form with password confirmation. Without advanced patterns, you end up with verbose if-else logic scattered across your codebase, duplicated validation, and fragile schemas that are hard to test.
Mastering these patterns leads to:
- Declarative clarity – Business rules live in the schema, not in imperative code.
- Reusability – Compose complex schemas from smaller, testable units.
- Performance – Avoid unnecessary re‑validation and reduce memory overhead.
- Type safety – Derive TypeScript types directly from your schemas.
How to Use Advanced Yup: Patterns
Conditional Validation with .when()
The .when() method lets you change a schema based on the value of another field. It’s perfect for dynamic forms.
import * as yup from 'yup';
const orderSchema = yup.object({
shipping: yup.string().oneOf(['standard', 'express', 'international']),
address: yup.object().when('shipping', {
is: 'international',
then: yup.object({
country: yup.string().required('Country is required for international shipping'),
postalCode: yup.string().required(),
}),
otherwise: yup.object({
state: yup.string().required(),
zip: yup.string().required(),
}),
}),
});
// Usage
orderSchema.validate({
shipping: 'international',
address: { country: 'Japan', postalCode: '100-0001' }
}).then(valid => console.log(valid));
For more complex conditions, you can pass an array of fields or use a function:
const conditionalSchema = yup.object({
hasDiscount: yup.boolean(),
discountCode: yup.string().when('hasDiscount', (hasDiscount, schema) => {
return hasDiscount ? schema.required() : schema.notRequired();
}),
});
Recursive Schemas for Tree‑like Data
Recursive validation is essential for nested structures like file trees, comments, or nested menus. Yup supports recursion via yup.lazy() or by referencing the schema itself.
// Define a node schema that references itself
const nodeSchema = yup.object({
id: yup.string().required(),
children: yup.array().of(
yup.lazy(() => nodeSchema) // lazy evaluation prevents infinite recursion
),
});
const tree = {
id: 'root',
children: [
{ id: 'child1', children: [] },
{ id: 'child2', children: [{ id: 'grandchild', children: [] }] }
]
};
nodeSchema.validate(tree); // passes
Note: Always wrap the recursive reference in yup.lazy() to avoid stack overflow on schema construction.
Cross‑Field Validation with .test()
When a validation rule depends on multiple fields, use .test() with access to the parent object via this.parent (or the options.context).
const passwordSchema = yup.object({
password: yup.string().min(8).required(),
confirmPassword: yup.string()
.test('passwords-match', 'Passwords must match', function(value) {
return this.parent.password === value;
}),
});
passwordSchema.validate({
password: 'secret123',
confirmPassword: 'secret123'
}); // ok
passwordSchema.validate({
password: 'secret123',
confirmPassword: 'different'
}); // throws ValidationError
For asynchronous checks (e.g., check username uniqueness), return a Promise:
yup.string()
.test('unique-username', 'Username already taken', async function(value) {
const isTaken = await checkDatabase(value);
return !isTaken;
});
Schema Composition and Reuse
Build complex schemas from smaller, well‑defined pieces. Use factories to generate schemas for repeated patterns.
// Base address schema
const addressSchema = yup.object({
street: yup.string().required(),
city: yup.string().required(),
country: yup.string().required(),
});
// Extend for different contexts
const billingAddressSchema = addressSchema.shape({
isPrimary: yup.boolean().default(false),
});
const shippingAddressSchema = addressSchema.shape({
instructions: yup.string().max(200),
});
// Schema factory
function createContactSchema(requirePhone = false) {
return yup.object({
name: yup.string().required(),
email: yup.string().email().required(),
phone: requirePhone ? yup.string().required() : yup.string(),
});
}
Performance Considerations
Validation can become a bottleneck in forms with many fields or when validating large datasets. Apply these patterns to keep Yup fast.
Lazy Evaluation with yup.lazy()
Avoid building schemas eagerly for every possible branch. yup.lazy() defers schema creation until the value is known.
// Instead of:
const schema = yup.mixed().oneOf([...]); // huge array built upfront
// Use:
const schema = yup.mixed().test('dynamic', 'Invalid', (value) => {
// compute allowed values lazily
const allowed = fetchAllowedValues(value);
return allowed.includes(value);
});
Memoize Schemas
If you create schemas inside React components or inside loops, cache them to avoid re‑creation. Use a simple memoization helper or useMemo.
import { useMemo } from 'react';
function useValidationSchema() {
return useMemo(() => y