# Zod Patterns Reference

## Overview

Zod is a TypeScript-first schema validation library. Use it for runtime validation at system boundaries (API responses, form inputs, external data).

## Basic Schema Patterns

### Primitive Types

```typescript
import { z } from 'zod';

// Basic types
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
const dateSchema = z.date();

// With constraints
const emailSchema = z.string().email();
const positiveNumber = z.number().positive();
const nonEmptyString = z.string().min(1);
const optionalString = z.string().optional();
const nullableString = z.string().nullable();
```

### Object Schemas

```typescript
// Basic object
const userSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  name: z.string().min(1),
  age: z.number().int().positive().optional(),
  role: z.enum(['admin', 'user', 'guest']),
  createdAt: z.string().datetime(),
});

// Infer TypeScript type from schema
type User = z.infer<typeof userSchema>;

// Partial and Pick utilities
const partialUser = userSchema.partial();           // All fields optional
const requiredUser = userSchema.required();         // All fields required
const pickedUser = userSchema.pick({ email: true, name: true });
const omittedUser = userSchema.omit({ createdAt: true });
```

### Array Schemas

```typescript
// Basic array
const stringArray = z.array(z.string());

// With constraints
const nonEmptyArray = z.array(z.string()).nonempty();
const limitedArray = z.array(z.number()).min(1).max(10);

// Tuple (fixed length, different types)
const coordinate = z.tuple([z.number(), z.number()]);
```

## API Response Validation

### Pattern: Validate API Responses

```typescript
// Define response schema
const apiResponseSchema = z.object({
  data: z.object({
    items: z.array(userSchema),
    total: z.number(),
    page: z.number(),
    pageSize: z.number(),
  }),
  meta: z.object({
    timestamp: z.string().datetime(),
    requestId: z.string().uuid(),
  }),
});

// In Angular service
@Injectable({ providedIn: 'root' })
export class UserService {
  #http = inject(HttpClient);
  #logger = logger({ component: 'UserService' });

  getUsers(): Observable<User[]> {
    return this.#http.get('/api/users').pipe(
      map((response) => {
        const result = apiResponseSchema.safeParse(response);
        if (!result.success) {
          this.#logger.error('Invalid API response', {
            errors: result.error.errors
          });
          throw new Error('Invalid API response');
        }
        return result.data.data.items;
      })
    );
  }
}
```

### Pattern: Coerce Types

```typescript
// API returns string IDs, coerce to number
const productSchema = z.object({
  id: z.coerce.number(),           // "123" -> 123
  price: z.coerce.number(),        // "99.99" -> 99.99
  inStock: z.coerce.boolean(),     // "true" -> true
  createdAt: z.coerce.date(),      // "2024-01-01" -> Date
});
```

## Form Validation

### Pattern: Form Schema

```typescript
// Define form schema with custom error messages
const loginFormSchema = z.object({
  email: z.string()
    .email({ message: 'Invalid email address' }),
  password: z.string()
    .min(8, { message: 'Password must be at least 8 characters' })
    .regex(/[A-Z]/, { message: 'Must contain uppercase letter' })
    .regex(/[0-9]/, { message: 'Must contain number' }),
  rememberMe: z.boolean().default(false),
});

// Use with Angular forms
type LoginForm = z.infer<typeof loginFormSchema>;
```

### Pattern: Cross-field Validation

```typescript
const passwordFormSchema = z.object({
  password: z.string().min(8),
  confirmPassword: z.string(),
}).refine(
  (data) => data.password === data.confirmPassword,
  {
    message: "Passwords don't match",
    path: ['confirmPassword'], // Error path
  }
);
```

## Type Guards

### Pattern: Create Type Guard from Schema

```typescript
// Schema
const customerSchema = z.object({
  type: z.literal('customer'),
  customerId: z.string(),
  loyaltyPoints: z.number(),
});

// Type guard function
function isCustomer(value: unknown): value is z.infer<typeof customerSchema> {
  return customerSchema.safeParse(value).success;
}

// Usage
if (isCustomer(data)) {
  console.log(data.loyaltyPoints); // Type-safe access
}
```

### Pattern: Discriminated Unions

```typescript
const customerSchema = z.object({
  type: z.literal('customer'),
  customerId: z.string(),
});

const guestSchema = z.object({
  type: z.literal('guest'),
  sessionId: z.string(),
});

const userSchema = z.discriminatedUnion('type', [
  customerSchema,
  guestSchema,
]);

type User = z.infer<typeof userSchema>;
// User = { type: 'customer'; customerId: string } | { type: 'guest'; sessionId: string }
```

## Replacing `any` Types

### Before (unsafe)

```typescript
function processOrder(order: any) {
  // No type safety
  console.log(order.items.length);
  console.log(order.customer.name);
}
```

### After (with Zod)

```typescript
const orderSchema = z.object({
  id: z.string().uuid(),
  items: z.array(z.object({
    productId: z.string(),
    quantity: z.number().positive(),
    price: z.number().nonnegative(),
  })),
  customer: z.object({
    name: z.string(),
    email: z.string().email(),
  }),
  status: z.enum(['pending', 'confirmed', 'shipped', 'delivered']),
});

type Order = z.infer<typeof orderSchema>;

function processOrder(input: unknown): Order {
  const order = orderSchema.parse(input); // Throws if invalid
  console.log(order.items.length);        // Type-safe
  console.log(order.customer.name);       // Type-safe
  return order;
}
```

## Error Handling

### Pattern: Structured Error Handling

```typescript
const result = schema.safeParse(data);

if (!result.success) {
  // Access formatted errors
  const formatted = result.error.format();

  // Access flat error list
  const flat = result.error.flatten();

  // Custom error mapping
  const errors = result.error.errors.map(err => ({
    field: err.path.join('.'),
    message: err.message,
    code: err.code,
  }));
}
```

## Transform Patterns

### Pattern: Transform on Parse

```typescript
const userInputSchema = z.object({
  email: z.string().email().transform(s => s.toLowerCase()),
  name: z.string().transform(s => s.trim()),
  tags: z.string().transform(s => s.split(',')),
});

// Input: { email: "USER@EXAMPLE.COM", name: "  John  ", tags: "a,b,c" }
// Output: { email: "user@example.com", name: "John", tags: ["a", "b", "c"] }
```

### Pattern: Default Values

```typescript
const configSchema = z.object({
  theme: z.enum(['light', 'dark']).default('light'),
  pageSize: z.number().default(20),
  features: z.array(z.string()).default([]),
});
```

## Best Practices

1. **Define schemas at module boundaries** - API services, form handlers
2. **Use `safeParse` for error handling** - Don't let validation throw unexpectedly
3. **Infer types from schemas** - Single source of truth
4. **Add meaningful error messages** - Help debugging and user feedback
5. **Use transforms for normalization** - Clean data on parse
6. **Keep schemas close to usage** - Colocate with services/components
