MSH

Data Validation with Zod

Published on
Last updated on
1import { z } from 'zod'; 2 3export const FormSchema = z.object({ 4 id: z.string(), 5 customerId: z.string({ 6 invalid_type_error: 'Please select a customer.', 7 }), 8 amount: z.coerce.number().gt(0, { message: 'Please enter an amount greater than $0.' }), 9 status: z.enum(['pending', 'paid'], { 10 invalid_type_error: 'Please select an invoice status.', 11 }), 12 date: z.string(), 13});

What is Zod?

According to the official website, Zod is a TypeScript-first schema validation library with static type inference. It helps ensure your data matches the expected shape and types at runtime, while providing excellent TypeScript integration.

The goal is to eliminate duplication type declarations. With Zod, you declare a validator once and Zod will automatically infer the static TypeScript type. This means you don't need to maintain separate type definitions and runtime validations.

Key Features:

  • Zero dependencies
  • Works in Node.js and all modern browsers
  • Tiny: 8kb minified + zipped
  • Immutable: methods (e.g. .optional()) return a new instance
  • Concise, chainable interface
  • Functional approach: parse, don't validate
  • Works with plain JavaScript too! You don't need to use TypeScript
  • Rich error messages and customizable error handling
  • Extensive validation primitives out of the box

Core Features and Usage

Basic Schema Definition and Validation

1import { z } from 'zod'; 2 3// Define a basic schema 4const UserSchema = z.object({ 5 name: z.string(), 6 age: z.number(), 7 email: z.string().email(), 8 isActive: z.boolean(), 9}); 10 11// Parsing data (throws on invalid data) 12try { 13 const user = UserSchema.parse({ 14 name: 'John', 15 age: 30, 16 email: 'john@example.com', 17 isActive: true, 18 }); 19} catch (error) { 20 console.error('Validation failed:', error); 21} 22 23// Safe parsing (returns success/error object) 24const result = UserSchema.safeParse(data); 25if (result.success) { 26 // Type is automatically inferred 27 const validatedData = result.data; 28} else { 29 // Handle ZodError 30 console.log(result.error.errors); 31}

Type Inference

Zod automatically infers TypeScript types from your schemas:

1import { z } from 'zod'; 2 3const UserSchema = z.object({ 4 id: z.string().uuid(), 5 name: z.string(), 6 age: z.number().optional(), 7}); 8 9// Extract the TypeScript type 10type User = z.infer<typeof UserSchema>; 11// Equivalent to: 12// type User = { 13 // id: string; 14 // name: string; 15 // age?: number; 16// }

Advanced Validation with Refinements

Zod offers two methods for custom validation: refine and superRefine:

1// Basic refinement 2const PasswordSchema = z 3 .string() 4 .min(8) 5 .refine((password) => /[A-Z]/.test(password), { 6 message: 'Password must contain at least one uppercase letter', 7 }); 8 9// Complex refinement with custom error handling 10const FormSchema = z 11 .object({ 12 password: z.string(), 13 confirm: z.string(), 14 }) 15 .superRefine((data, ctx) => { 16 if (data.password !== data.confirm) { 17 ctx.addIssue({ 18 code: z.ZodIssueCode.custom, 19 message: "Passwords don't match", 20 path: ['confirm'], // Path of the error 21 }); 22 } 23 });

Schema Composition and Extension

Zod makes it easy to compose and extend schemas:

1// Base schema 2const BaseSchema = z.object({ 3 id: z.string().uuid(), 4 createdAt: z.date(), 5 updatedAt: z.date(), 6}); 7 8// Extended schema 9const UserSchema = BaseSchema.extend({ 10 username: z.string().min(3), 11 email: z.string().email(), 12 settings: z.object({ 13 theme: z.enum(['light', 'dark']), 14 notifications: z.boolean(), 15 }), 16}); 17 18// Partial schema (all fields optional) 19const PartialUserSchema = UserSchema.partial(); 20 21// Deep partial (nested fields also optional) 22const DeepPartialUserSchema = UserSchema.deepPartial();

Transform and Preprocess

Zod allows you to transform data during validation:

1const StringToNumberSchema = z.string().transform((str) => parseInt(str, 10)); 2 3const DateStringSchema = z.string().transform((str) => new Date(str)); 4 5const UserInputSchema = z.object({ 6 name: z.string().transform((str) => str.trim()), 7 age: z 8 .string() 9 .transform((str) => parseInt(str)) 10 .pipe(z.number().min(18)), 11});

Union Types and Discriminated Unions

1// Simple union 2const StringOrNumber = z.union([z.string(), z.number()]); 3 4// Discriminated union 5const Shape = z.discriminatedUnion('type', [ 6 z.object({ type: z.literal('circle'), radius: z.number() }), 7 z.object({ type: z.literal('square'), sideLength: z.number() }), 8]);

Error Handling

Zod provides detailed error information:

1const schema = z.object({ 2 email: z.string().email(), 3 age: z.number().min(18), 4}); 5 6try { 7 schema.parse({ email: 'invalid', age: 16 }); 8} catch (error) { 9 if (error instanceof z.ZodError) { 10 console.log(error.errors); // Array of validation issues 11 console.log(error.flatten()); // Flattened error structure 12 } 13}

Advanced Features

Optional and Nullable Fields

1const UserSchema = z.object({ 2 name: z.string(), 3 email: z.string().email().optional(), // Field can be undefined 4 phone: z.string().nullable(), // Field can be null 5 age: z.number().optional().nullable(), // Field can be undefined or null 6});

Array Validation

1const NumberArraySchema = z.array(z.number()); 2const StringArraySchema = z.string().array(); // Alternative syntax 3const TuppleSchema = z.tuple([z.string(), z.number()]); // Fixed-length array

Common Validation Patterns

1// Empty string handling 2const NonEmptyString = z.string().min(1, 'String cannot be empty'); 3 4// Custom string formats 5const URLSchema = z.string().url(); 6const UUIDSchema = z.string().uuid(); 7const EmailSchema = z.string().email(); 8 9// Numeric validations 10const PositiveNumber = z.number().positive(); 11const IntegerSchema = z.number().int(); 12const RangeSchema = z.number().min(0).max(100); 13 14// Date validation 15const FutureDateSchema = z.date().min(new Date()); 16const PastDateSchema = z.date().max(new Date());

Best Practices

  1. Reuse Common Schemas
1const BaseSchema = z.object({ 2 id: z.string().uuid(), 3 createdAt: z.date(), 4 updatedAt: z.date(), 5}); 6 7const UserSchema = BaseSchema.extend({ 8 name: z.string(), 9 email: z.string().email(), 10});
  1. Custom Error Messages
1const FormSchema = z.object({ 2 username: z 3 .string({ 4 required_error: 'Username is required', 5 invalid_type_error: 'Username must be a string', 6 }) 7 .min(3, 'Username must be at least 3 characters'), 8});
  1. Use Transformations for Data Cleanup
1const UserInputSchema = z.object({ 2 email: z 3 .string() 4 .email() 5 .transform((e) => e.toLowerCase()), 6 name: z.string().transform((n) => n.trim()), 7 age: z.string().transform((a) => parseInt(a, 10)), 8});

Common Use Cases

  1. API Payload Validation
1const APIRequestSchema = z.object({ 2 query: z.string().optional(), 3 page: z.number().int().positive().default(1), 4 limit: z.number().int().min(1).max(100).default(10), 5});
  1. Environment Variables Validation
1const EnvSchema = z.object({ 2 DATABASE_URL: z.string().url(), 3 API_KEY: z.string().min(1), 4 PORT: z.string().transform((str) => parseInt(str, 10)), 5});
  1. Configuration Validation
1const ConfigSchema = z.object({ 2 server: z.object({ 3 host: z.string(), 4 port: z.number(), 5 }), 6 database: z.object({ 7 url: z.string().url(), 8 timeout: z.number().positive(), 9 }), 10 features: z.record(z.boolean()), 11});

Conclusion

Zod has become a handy tool for developers dealing with form validation, especially when combined with TypeScript. Using Zod's schema declaration and validation features, developers can ensure that the data entered into forms aligns with the specified structure and adheres to predefined validation rules.

References

GET IN TOUCH

Let's work together

I build exceptional and accessible digital experiences for the web

WRITE AN EMAIL

or reach out directly at hello@mohammadshehadeh.com