Yup
Yup — schema-based object validation with casting, transforms, conditional rules, localized error messages, and deep Formik integration
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 linesYup — 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 fromvalidatewhen 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 becomes5for number schemas. If you want strict type checking without coercion, useschema.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 sameyup.object(). For cross-object validation, use.test()with a function that receives the full context viathis.parentorthis.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
Related Skills
Conform
Conform — progressive enhancement forms, Server Actions, Zod integration, nested objects, arrays, intent buttons, and accessibility patterns
Formik
Formik — useFormik, Field, Form, validation with Yup, FieldArray, custom inputs, submission handling, and error display patterns
React Final Form
React Final Form — subscription-based form state management with fine-grained re-renders, field-level validation, decorator support, and zero dependencies
React Hook Form
React Hook Form — useForm, register, Controller, validation, nested fields, arrays (useFieldArray), DevTools, and performance optimization patterns
Valibot
Valibot — modular schema validation, tiny bundle size, parse/safeParse, pipe transformations, type inference, and comparison with Zod
Vest
Vest — unit-test-inspired form validation framework with suite/test syntax, async rules, group support, warning-level validations, and framework-agnostic design