Skip to main content
Technology & EngineeringForms Validation264 lines

Yup

Yup — schema-based object validation with casting, transforms, conditional rules, localized error messages, and deep Formik integration

Quick Summary15 lines
You are an expert in using Yup for form handling and validation.

## Key Points

- Always pass `{ abortEarly: false }` when validating full forms so users see all errors at once rather than fixing them one at a time.
- Use `schema.cast(data)` separately from `validate` when you need the coerced/transformed data without throwing on optional fields — this is useful for normalizing API input before persistence.
- Leverage `yup.ref()` for cross-field validation (confirm password, date ranges) instead of writing custom tests that manually read sibling values.

## Quick Example

```bash
npm install yup
```
skilldb get forms-validation-skills/YupFull skill: 264 lines
Paste into your CLAUDE.md or agent config

Yup — Forms & Validation

You are an expert in using Yup for form handling and validation.

Core Philosophy

Overview

Yup is a schema builder for runtime value parsing and validation. It defines schemas with a chainable API, performs type coercion (casting) by default, supports sync and async validation, conditional rules, custom tests, and fine-grained error messages. Yup is the most widely used validation library in the React ecosystem thanks to its first-class integration with Formik and React Hook Form (via resolvers). It works in any JavaScript environment — Node, browsers, and React Native.

Setup & Configuration

npm install yup

Basic schema:

import * as yup from "yup";

const userSchema = yup.object({
  name: yup.string().required("Name is required").min(2, "At least 2 characters"),
  email: yup.string().required("Email is required").email("Invalid email address"),
  age: yup.number().required().positive().integer().min(13, "Must be at least 13"),
  website: yup.string().url("Must be a valid URL").nullable(),
});

// Infer TypeScript type from schema
type User = yup.InferType<typeof userSchema>;
// { name: string; email: string; age: number; website: string | null }

Validation:

// Throws ValidationError on failure
try {
  const user = await userSchema.validate(inputData);
  console.log(user); // cast + validated
} catch (err) {
  console.log(err.errors); // string[] of error messages
}

// Non-throwing variant
const isValid = await userSchema.isValid(inputData); // boolean

// Validate and collect ALL errors (not just the first)
try {
  await userSchema.validate(inputData, { abortEarly: false });
} catch (err) {
  console.log(err.inner); // ValidationError[] per field
}

Core Patterns

Casting and Transforms

Yup coerces values by default — "5" becomes 5 for a number schema. You can add custom transforms:

const schema = yup.object({
  email: yup
    .string()
    .required()
    .email()
    .transform((value) => value.toLowerCase().trim()),
  tags: yup
    .array()
    .of(yup.string())
    .transform((value) =>
      typeof value === "string" ? value.split(",").map((t) => t.trim()) : value
    ),
});

// { email: "  Alice@Example.COM " } → { email: "alice@example.com" }
// { tags: "react, vue, svelte" }    → { tags: ["react", "vue", "svelte"] }

Conditional Validation with when

const schema = yup.object({
  hasCompany: yup.boolean(),
  companyName: yup.string().when("hasCompany", {
    is: true,
    then: (schema) => schema.required("Company name is required"),
    otherwise: (schema) => schema.notRequired(),
  }),
  // Shorthand for `is: true`
  role: yup.string().when("hasCompany", ([hasCompany], schema) =>
    hasCompany ? schema.oneOf(["admin", "member"]) : schema.strip()
  ),
});

Custom Test Methods

const schema = yup.object({
  password: yup
    .string()
    .required()
    .min(8)
    .test(
      "no-common-passwords",
      "Password is too common",
      (value) => !["password", "12345678", "qwerty"].includes(value ?? "")
    ),
  confirmPassword: yup
    .string()
    .required()
    .oneOf([yup.ref("password")], "Passwords must match"),
});

Async custom test (e.g., checking server-side uniqueness):

const schema = yup.object({
  username: yup
    .string()
    .required()
    .test("unique", "Username is already taken", async (value) => {
      if (!value || value.length < 3) return true; // skip check
      const res = await fetch(`/api/check-username?u=${value}`);
      const { available } = await res.json();
      return available;
    }),
});

Nested Objects and Arrays

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

const orderSchema = yup.object({
  customer: yup.string().required(),
  shippingAddress: addressSchema.required(),
  items: yup
    .array()
    .of(
      yup.object({
        productId: yup.string().required(),
        quantity: yup.number().required().positive().integer(),
      })
    )
    .min(1, "At least one item is required"),
});

Using with Formik

import { Formik, Form, Field, ErrorMessage } from "formik";

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

function LoginForm() {
  return (
    <Formik
      initialValues={{ email: "", password: "" }}
      validationSchema={validationSchema}
      onSubmit={(values) => console.log(values)}
    >
      <Form>
        <Field name="email" type="email" />
        <ErrorMessage name="email" component="span" />

        <Field name="password" type="password" />
        <ErrorMessage name="password" component="span" />

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

Using with React Hook Form

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

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

function MyForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: yupResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input {...register("name")} />
      {errors.name && <span>{errors.name.message}</span>}
      <input {...register("email")} />
      {errors.email && <span>{errors.email.message}</span>}
      <button type="submit">Submit</button>
    </form>
  );
}

Localized Error Messages

import { setLocale } from "yup";

setLocale({
  mixed: {
    required: "${path} is required",
    notType: "${path} must be a valid ${type}",
  },
  string: {
    email: "${path} must be a valid email",
    min: "${path} must be at least ${min} characters",
  },
  number: {
    min: "${path} must be at least ${min}",
  },
});

Best Practices

  • Always pass { abortEarly: false } when validating full forms so users see all errors at once rather than fixing them one at a time.
  • Use schema.cast(data) separately from validate when you need the coerced/transformed data without throwing on optional fields — this is useful for normalizing API input before persistence.
  • Leverage yup.ref() for cross-field validation (confirm password, date ranges) instead of writing custom tests that manually read sibling values.

Common Pitfalls

  • Yup casts values by default, which means "5" silently becomes 5 for number schemas. If you want strict type checking without coercion, use schema.validate(data, { strict: true }) or call .strict() on the schema.
  • Using yup.ref() outside of the same object level does not work — refs only resolve siblings within the same yup.object(). For cross-object validation, use .test() with a function that receives the full context via this.parent or this.from.

Anti-Patterns

Over-engineering for hypothetical requirements. Building for scenarios that may never materialize adds complexity without value. Solve the problem in front of you first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide wastes time and introduces risk.

Premature abstraction. Creating elaborate frameworks before having enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at system boundaries. Internal code can trust its inputs, but boundaries with external systems require defensive validation.

Skipping documentation. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add forms-validation-skills

Get CLI access →