Validating Data with Yup and Testing Schemas Using Jest
When building forms or handling user input, robust validation is essential. Instead of writing custom logic for every field, leveraging a schema-based validation library like Yup streamlines the process and improves maintainability.
Yup allows defining schemas that describe expected data structures, including required fields, type constraints, value ranges, and conditional logic. For example:
import { object, string, number, boolean } from 'yup';
const userSchema = object({
name: string().required(),
age: number().min(18).max(120).required(),
country: string().oneOf(['US', 'DE', 'JP']).required(),
newsletter: boolean().default(false),
});
This schema enforces that name and age are present, age falls within a valid range, and country matches one of the allowed values.
To support dynamic enums—such as those loaded from an API or environment-dependent options—the schema must be constructed at runtime. Since Yup schemas are immutable, updating parts of a schema requires creating a new instance using .shape():
const baseSchema = object({
name: string().required(),
});
// Dynamically extend based on runtime data
const fullSchema = baseSchema.shape({
role: string().oneOf(availableRoles).required(),
});
Testing these schemas ensures correctness under various conditions. Jest integrates well for this purpose. A typical test validates both valid inputs and edge cases that should fail:
import { userSchema } from './schema';
describe('User Schema Validation', () => {
it('accepts valid input', async () => {
const validData = {
name: 'Alice',
age: 30,
country: 'DE',
newsletter: true,
};
await expect(userSchema.validate(validData)).resolves.toEqual(validData);
});
it('rejects age below minimum', async () => {
const invalidData = {
name: 'Bob',
age: 15,
country: 'US',
};
await expect(userSchema.validate(invalidData)).rejects.toThrow();
});
it('rejects invalid country code', async () => {
const invalidData = {
name: 'Charlie',
age: 25,
country: 'XX',
};
await expect(userSchema.validate(invalidData)).rejects.toThrow();
});
});
For dynamic enum values sourced from a function (e.g., getAllowedRoles()), mocking that function in tests allows verifying different configurations:
jest.mock('./roles', () => ({
getAllowedRoles: jest.fn(),
}));
it('validates against mocked roles', async () => {
const mockRoles = ['admin', 'user'];
(getAllowedRoles as jest.Mock).mockReturnValue(mockRoles);
const dynamicSchema = baseSchema.shape({
role: string().oneOf(mockRoles).required(),
});
await expect(dynamicSchema.validate({ name: 'Test', role: 'admin' })).resolves.not.toThrow();
await expect(dynamicSchema.validate({ name: 'Test', role: 'guest' })).rejects.toThrow();
});
This approach decouples validation logic from UI components, enables reuse across insert and update operations (with optional fields handled via .nullable() or conditional rules), and ensures reliability through automated tests.