References

Yup Validation

Schema-based validation for JavaScript and TypeScript with async support and built-in Formik integration. Best for legacy projects or when Formik is in use.

Dependencies:

{
  "yup": ">=1.0.0 <2.0.0"
}

Core Patterns

✅ REQUIRED: Use InferType for Type Extraction

// ✅ CORRECT: Schema as source of truth
import * as yup from 'yup';

const userSchema = yup.object({
  name: yup.string().required(),
  email: yup.string().email().required(),
  age: yup.number().positive().integer(),
});

type User = yup.InferType<typeof userSchema>;
// Inferred: { name: string; email: string; age?: number }

// ❌ WRONG: Manual interface (can drift)
interface User {
  name: string;
  email: string;
  age: number;
}

✅ REQUIRED: Handle Validation Errors

// ✅ CORRECT: Proper error handling
try {
  const validated = await userSchema.validate(data);
  console.log(validated);
} catch (error) {
  if (error instanceof yup.ValidationError) {
    console.error(error.message);
    console.error(error.path); // Field that failed
    console.error(error.errors); // Array of error messages
  }
}

// Get all errors (not just first)
try {
  await schema.validate(data, { abortEarly: false });
} catch (error) {
  if (error instanceof yup.ValidationError) {
    error.inner.forEach((err) => {
      console.log(err.path, err.message);
    });
  }
}

✅ REQUIRED: Chain Validations

// ✅ CORRECT: Multiple constraints
const email = yup
  .string()
  .email('Invalid email format')
  .required('Email is required')
  .max(100, 'Email too long');

const password = yup
  .string()
  .required('Password is required')
  .min(8, 'Password must be at least 8 characters')
  .matches(/[A-Z]/, 'Must contain uppercase letter')
  .matches(/[0-9]/, 'Must contain number');

// ❌ WRONG: Single validation
const email = yup.string(); // Too permissive

Common Validations

String Validations

yup.string(); // Any string
yup.string().min(3); // Min length
yup.string().max(20); // Max length
yup.string().length(10); // Exact length
yup.string().email(); // Valid email
yup.string().url(); // Valid URL
yup.string().matches(/^\d{3}-\d{3}-\d{4}$/); // Regex pattern
yup.string().trim(); // Trim whitespace
yup.string().lowercase(); // Transform to lowercase
yup.string().uppercase(); // Transform to uppercase
yup.string().required(); // Not empty/null/undefined

Number Validations

yup.number(); // Any number
yup.number().integer(); // Integer only
yup.number().positive(); // > 0
yup.number().negative(); // < 0
yup.number().min(18); // Minimum value
yup.number().max(65); // Maximum value
yup.number().lessThan(100); // < value
yup.number().moreThan(0); // > value
yup.number().required(); // Not null/undefined

Optional and Nullable

yup.string().notRequired(); // undefined allowed
yup.string().nullable(); // null allowed
yup.string().optional(); // Alias for .notRequired()
yup.string().default('N/A'); // Default value
yup.string().defined(); // Not undefined (null allowed)

Arrays and Objects

// Array validation
const tagsSchema = yup.array()
  .of(yup.string())
  .min(1, 'At least one tag required')
  .max(5, 'Maximum 5 tags');

// Nested object
const addressSchema = yup.object({
  street: yup.string().required(),
  city: yup.string().required(),
  zipCode: yup.string().matches(/^\d{5}$/),
});

const userSchema = yup.object({
  name: yup.string().required(),
  address: addressSchema,
});

Advanced Patterns

Conditional Validation (when)

// Dependent field validation
const schema = yup.object({
  isCompany: yup.boolean(),
  companyName: yup.string().when('isCompany', {
    is: true,
    then: (schema) => schema.required('Company name required'),
    otherwise: (schema) => schema.notRequired(),
  }),
});

// Multiple conditions
const schema = yup.object({
  age: yup.number(),
  driversLicense: yup.string().when('age', {
    is: (age: number) => age >= 16,
    then: (schema) => schema.required(),
  }),
});

Custom Test Methods

// Single custom validation
const usernameSchema = yup
  .string()
  .test('unique-username', 'Username already taken', async (value) => {
    if (!value) return true; // Skip if empty (let required() handle)
    const available = await checkUsernameAvailable(value);
    return available;
  });

// Multiple tests
const passwordSchema = yup
  .string()
  .test('has-uppercase', 'Must contain uppercase', (val) => /[A-Z]/.test(val))
  .test('has-number', 'Must contain number', (val) => /[0-9]/.test(val));

Transform Values

// Transform during validation
const trimmedString = yup
  .string()
  .transform((value) => (value ? value.trim() : value));

const numberFromString = yup
  .string()
  .transform((value) => (value ? Number.parseInt(value, 10) : value));

// Transform object
const schema = yup.object({
  name: yup.string().trim().lowercase(),
  tags: yup.array().transform((val) => val || []), // Default to empty array
});

Schema Composition

// Base schema
const basePersonSchema = yup.object({
  firstName: yup.string().required(),
  lastName: yup.string().required(),
});

// Extend with concat
const employeeSchema = basePersonSchema.concat(
  yup.object({
    employeeId: yup.string().required(),
    department: yup.string().required(),
  })
);

// Reusable schemas
const emailField = yup.string().email().required();
const passwordField = yup.string().min(8).required();

const loginSchema = yup.object({
  email: emailField,
  password: passwordField,
});

Error Handling

Validation Options

// Get all errors (not just first)
try {
  await schema.validate(data, { abortEarly: false });
} catch (error) {
  // error.inner contains all validation errors
}

// Strip unknown keys (default behavior)
await schema.validate(data, { stripUnknown: true });

// Strict mode (error on unknown keys)
await schema.validate(data, { strict: true });

// Context for conditional validation
await schema.validate(data, { context: { userId: 123 } });

Custom Error Messages

const schema = yup.object({
  email: yup
    .string()
    .required('Email is required')
    .email('Please enter a valid email'),

  age: yup
    .number()
    .required('Age is required')
    .min(18, 'You must be at least 18 years old')
    .max(120, 'Please enter a valid age'),
});

Integration Examples

Formik + Yup (Native Integration)

import { Formik, Form, Field } from 'formik';
import * as yup from 'yup';

const validationSchema = yup.object({
  email: yup.string().email('Invalid email').required('Required'),
  password: yup.string().min(8, 'Too short').required('Required'),
});

function LoginForm() {
  return (
    <Formik
      initialValues={{ email: '', password: '' }}
      validationSchema={validationSchema}
      onSubmit={(values) => console.log(values)}
    >
      {({ errors, touched }) => (
        <Form>
          <Field name="email" type="email" />
          {errors.email && touched.email && <div>{errors.email}</div>}

          <Field name="password" type="password" />
          {errors.password && touched.password && <div>{errors.password}</div>}

          <button type="submit">Submit</button>
        </Form>
      )}
    </Formik>
  );
}

React Hook Form + Yup

import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';

const schema = yup.object({
  email: yup.string().email().required(),
  password: yup.string().min(8).required(),
});

type FormData = yup.InferType<typeof schema>;

function LoginForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
    resolver: yupResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit(data => console.log(data))}>
      <input {...register('email')} />
      {errors.email && <span>{errors.email.message}</span>}

      <button type="submit">Submit</button>
    </form>
  );
}

Express API Validation

import * as yup from 'yup';
import { Request, Response, NextFunction } from 'express';

const createUserSchema = yup.object({
  body: yup.object({
    name: yup.string().required(),
    email: yup.string().email().required(),
    age: yup.number().positive().integer(),
  }),
});

async function validateRequest(schema: yup.Schema) {
  return async (req: Request, res: Response, next: NextFunction) => {
    try {
      await schema.validate({ body: req.body }, { abortEarly: false });
      next();
    } catch (error) {
      if (error instanceof yup.ValidationError) {
        return res.status(400).json({ errors: error.errors });
      }
      next(error);
    }
  };
}

app.post('/users', validateRequest(createUserSchema), (req, res) => {
  const user = req.body; // Validated
  res.json({ success: true });
});

Edge Cases

Circular references: Use yup.lazy() for recursive schemas.

interface Category {
  name: string;
  subcategories: Category[];
}

const categorySchema: yup.Schema<Category> = yup.object({
  name: yup.string().required(),
  subcategories: yup.lazy(() => yup.array(categorySchema)),
});

Nullable vs notRequired: .nullable() allows null, .notRequired() allows undefined.

yup.string().nullable(); // string | null
yup.string().notRequired(); // string | undefined
yup.string().nullable().notRequired(); // string | null | undefined

Strict mode: By default, Yup removes unknown keys. Use .strict() to throw error.

const strict = schema.strict(); // Throws on unknown keys

Synchronous validation: Use .validateSync() — async tests not allowed.

try {
  const valid = schema.validateSync(data);
} catch (error) {
  // Handle error
}

Migration from Zod

ZodYup
z.string()yup.string()
z.number()yup.number()
z.object({ ... })yup.object({ ... })
z.array(schema)yup.array().of(schema)
.optional().notRequired() or .optional()
.nullable().nullable()
.default(val).default(val)
z.infer<typeof schema>yup.InferType<typeof schema>
.parse().validateSync()
.safeParse()try/catch with .validate()
.refine().test()
.transform().transform()


References