> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Automatic Validation

> Type-safe parameter validation with Zod

Sonamu can automatically validate API parameters using Zod.

## Zod Validation Overview

<CardGroup cols={2}>
  <Card title="Type Safety" icon="shield-check">
    Compile-time validation Automated runtime validation
  </Card>

  <Card title="Automatic Validation" icon="wand-magic-sparkles">
    Schema-based validation Auto-generated error messages
  </Card>

  <Card title="Type Inference" icon="code">
    TypeScript type generation Autocomplete support
  </Card>

  <Card title="Extensible" icon="puzzle-piece">
    Custom validation rules Transform support
  </Card>
</CardGroup>

## Installing Zod

```bash theme={null}
npm install zod
# or
pnpm add zod
```

<Info>Sonamu supports Zod v4. Install the latest version.</Info>

## Basic Usage

### Defining Zod Schemas

```typescript theme={null}
import { z } from "zod";
import { BaseModelClass, api } from "sonamu";

// Define Zod schema
const CreateUserSchema = z.object({
  email: z.string().email("Invalid email format"),
  username: z.string().min(3, "Username must be at least 3 characters").max(20),
  password: z.string().min(8, "Password must be at least 8 characters"),
  age: z.number().int().min(18, "Must be at least 18 years old").optional(),
  role: z.enum(["admin", "manager", "normal"]),
});

// TypeScript type inference
type CreateUserParams = z.infer<typeof CreateUserSchema>;

class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  @api({ httpMethod: "POST" })
  async create(params: CreateUserParams): Promise<{ userId: number }> {
    // Validate with schema
    const validated = CreateUserSchema.parse(params);

    const wdb = this.getPuri("w");
    const [user] = await wdb.table("users").insert(validated).returning("id");

    return { userId: user.id };
  }
}
```

### Handling Validation Errors

```typescript theme={null}
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  @api({ httpMethod: "POST" })
  async create(params: unknown): Promise<{ userId: number }> {
    try {
      // Validate
      const validated = CreateUserSchema.parse(params);

      const wdb = this.getPuri("w");
      const [user] = await wdb.table("users").insert(validated).returning("id");

      return { userId: user.id };
    } catch (error) {
      if (error instanceof z.ZodError) {
        // Zod validation error
        const messages = error.errors.map((e) => e.message).join(", ");
        throw new Error(`Validation failed: ${messages}`);
      }
      throw error;
    }
  }
}
```

## Zod Schema Types

### Primitive Types

```typescript theme={null}
import { z } from "zod";

// String
const StringSchema = z.string();
const EmailSchema = z.string().email();
const UrlSchema = z.string().url();
const UuidSchema = z.string().uuid();

// Number
const NumberSchema = z.number();
const IntSchema = z.number().int();
const PositiveSchema = z.number().positive();
const MinMaxSchema = z.number().min(0).max(100);

// Boolean
const BooleanSchema = z.boolean();

// Date
const DateSchema = z.date();
const DateStringSchema = z.string().datetime(); // ISO 8601
```

### Objects

```typescript theme={null}
const UserSchema = z.object({
  email: z.string().email(),
  username: z.string().min(3).max(20),
  age: z.number().int().min(18).optional(),
  profile: z
    .object({
      bio: z.string().max(500).optional(),
      avatarUrl: z.string().url().optional(),
      socialLinks: z
        .object({
          twitter: z.string().url().optional(),
          github: z.string().url().optional(),
        })
        .optional(),
    })
    .optional(),
});

type User = z.infer<typeof UserSchema>;
// {
//   email: string;
//   username: string;
//   age?: number;
//   profile?: {
//     bio?: string;
//     avatarUrl?: string;
//     socialLinks?: {
//       twitter?: string;
//       github?: string;
//     };
//   };
// }
```

### Arrays

```typescript theme={null}
// String array
const StringArraySchema = z.array(z.string());

// Object array
const UserArraySchema = z.array(
  z.object({
    id: z.number(),
    name: z.string(),
  }),
);

// Min/max length
const ArraySchema = z.array(z.string()).min(1).max(10);

// Non-empty array
const NonEmptyArraySchema = z.array(z.string()).nonempty();
```

### Enum and Literal

```typescript theme={null}
// Enum
const RoleSchema = z.enum(["admin", "manager", "normal"]);
type Role = z.infer<typeof RoleSchema>; // "admin" | "manager" | "normal"

// Literal (Zod v4 supports multiple values)
const StatusSchema = z.literal(["active", "inactive", "pending"]);

// Union
const RoleOrStatusSchema = z.union([z.literal("admin"), z.literal("user"), z.literal("guest")]);
```

### Optional and Nullable

```typescript theme={null}
// Optional (allows undefined)
const OptionalSchema = z.string().optional();
// string | undefined

// Nullable (allows null)
const NullableSchema = z.string().nullable();
// string | null

// Both allowed
const NullishSchema = z.string().nullish();
// string | null | undefined

// Default value
const DefaultSchema = z.string().default("default value");
```

## Practical Examples

### User Registration

```typescript theme={null}
const RegisterSchema = z
  .object({
    email: z.string().email("Please enter a valid email"),
    username: z
      .string()
      .min(3, "Username must be at least 3 characters")
      .max(20, "Username can be up to 20 characters")
      .regex(/^[a-zA-Z0-9_]+$/, "Username can only contain letters, numbers, and underscores"),
    password: z
      .string()
      .min(8, "Password must be at least 8 characters")
      .regex(/[A-Z]/, "Password must contain at least 1 uppercase letter")
      .regex(/[a-z]/, "Password must contain at least 1 lowercase letter")
      .regex(/[0-9]/, "Password must contain at least 1 number"),
    confirmPassword: z.string(),
    agreeToTerms: z.boolean().refine((val) => val === true, {
      message: "You must agree to the terms of service",
    }),
  })
  .refine((data) => data.password === data.confirmPassword, {
    message: "Passwords do not match",
    path: ["confirmPassword"],
  });

type RegisterParams = z.infer<typeof RegisterSchema>;

class AuthModelClass extends BaseModelClass<
  AuthSubsetKey,
  AuthSubsetMapping,
  typeof authSubsetQueries,
  typeof authLoaderQueries
> {
  constructor() {
    super("Auth", authSubsetQueries, authLoaderQueries);
  }

  @api({ httpMethod: "POST" })
  async register(params: RegisterParams): Promise<{ userId: number }> {
    const validated = RegisterSchema.parse(params);

    const wdb = this.getPuri("w");

    // Save excluding confirmPassword
    const { confirmPassword, agreeToTerms, ...userData } = validated;

    const [user] = await wdb
      .table("users")
      .insert({
        ...userData,
        role: "normal",
      })
      .returning("id");

    return { userId: user.id };
  }
}
```

### Pagination Parameters

```typescript theme={null}
const PaginationSchema = z.object({
  page: z.number().int().positive().default(1),
  pageSize: z.number().int().min(1).max(100).default(20),
  sortBy: z.enum(["created_at", "updated_at", "name"]).optional(),
  sortOrder: z.enum(["asc", "desc"]).default("desc"),
});

const UserListSchema = PaginationSchema.extend({
  search: z.string().optional(),
  role: z.enum(["admin", "manager", "normal"]).optional(),
  isActive: z.boolean().optional(),
});

type UserListParams = z.infer<typeof UserListSchema>;

class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  @api({ httpMethod: "GET" })
  async list(params: UserListParams): Promise<{
    users: User[];
    total: number;
    page: number;
    pageSize: number;
  }> {
    const validated = UserListSchema.parse(params);

    const rdb = this.getPuri("r");

    let query = rdb.table("users");

    if (validated.search) {
      query = query.where("username", "like", `%${validated.search}%`);
    }

    if (validated.role) {
      query = query.where("role", validated.role);
    }

    if (validated.isActive !== undefined) {
      query = query.where("is_active", validated.isActive);
    }

    const users = await query
      .orderBy(validated.sortBy || "created_at", validated.sortOrder)
      .limit(validated.pageSize)
      .offset((validated.page - 1) * validated.pageSize)
      .select("*");

    const [{ count }] = await rdb.table("users").count({ count: "*" });

    return {
      users,
      total: count,
      page: validated.page,
      pageSize: validated.pageSize,
    };
  }
}
```

### Date Range Validation

```typescript theme={null}
const DateRangeSchema = z
  .object({
    startDate: z.string().datetime(),
    endDate: z.string().datetime(),
  })
  .refine((data) => new Date(data.startDate) < new Date(data.endDate), {
    message: "Start date must be before end date",
    path: ["endDate"],
  });

const ReportSchema = z.object({
  dateRange: DateRangeSchema,
  type: z.enum(["sales", "traffic", "conversion"]),
  format: z.enum(["pdf", "excel", "csv"]).default("pdf"),
});

type ReportParams = z.infer<typeof ReportSchema>;

class ReportModelClass extends BaseModelClass<
  ReportSubsetKey,
  ReportSubsetMapping,
  typeof reportSubsetQueries,
  typeof reportLoaderQueries
> {
  constructor() {
    super("Report", reportSubsetQueries, reportLoaderQueries);
  }

  @api({ httpMethod: "POST" })
  async generate(params: ReportParams): Promise<{ reportId: number }> {
    const validated = ReportSchema.parse(params);

    // Report generation logic
    // ...
  }
}
```

## Transform (Data Transformation)

### String Transformation

```typescript theme={null}
const TrimmedStringSchema = z.string().transform((val) => val.trim());

const LowercaseEmailSchema = z
  .string()
  .email()
  .transform((val) => val.toLowerCase());

const UserSchema = z.object({
  email: z
    .string()
    .email()
    .transform((val) => val.toLowerCase()),
  username: z.string().transform((val) => val.trim()),
});
```

### Number Transformation

```typescript theme={null}
// Convert string to number
const StringToNumberSchema = z.string().transform((val) => parseInt(val, 10));

// Coerce (forced conversion)
const CoerceNumberSchema = z.coerce.number();
// "123" → 123
// true → 1
// false → 0
```

### Date Transformation

```typescript theme={null}
// ISO string to Date object
const DateFromStringSchema = z
  .string()
  .datetime()
  .transform((val) => new Date(val));

// Coerce
const CoerceDateSchema = z.coerce.date();
// "2025-01-07" → Date object
// "2025-01-07T12:00:00Z" → Date object
```

### Complex Transformation

```typescript theme={null}
const ProductSchema = z.object({
  name: z.string(),
  price: z.string().transform((val) => parseFloat(val)),
  tags: z.string().transform((val) => val.split(",").map((t) => t.trim())),
  metadata: z.string().transform((val) => {
    try {
      return JSON.parse(val);
    } catch {
      return {};
    }
  }),
});

// Input:
// {
//   name: "Product",
//   price: "29.99",
//   tags: "electronics, gadget, new",
//   metadata: '{"color": "blue", "size": "M"}'
// }

// Output:
// {
//   name: "Product",
//   price: 29.99,
//   tags: ["electronics", "gadget", "new"],
//   metadata: { color: "blue", size: "M" }
// }
```

## Custom Validation

### Using refine()

```typescript theme={null}
const PasswordSchema = z
  .string()
  .min(8)
  .refine((val) => /[A-Z]/.test(val), {
    message: "Uppercase required",
  })
  .refine((val) => /[a-z]/.test(val), {
    message: "Lowercase required",
  })
  .refine((val) => /[0-9]/.test(val), {
    message: "Number required",
  });

// Validate entire object
const UserSchema = z
  .object({
    password: z.string(),
    confirmPassword: z.string(),
  })
  .refine((data) => data.password === data.confirmPassword, {
    message: "Passwords do not match",
    path: ["confirmPassword"],
  });
```

### Using superRefine() (Complex Validation)

```typescript theme={null}
const ComplexSchema = z
  .object({
    startDate: z.date(),
    endDate: z.date(),
    participants: z.array(z.string()),
  })
  .superRefine((data, ctx) => {
    // Date validation
    if (data.startDate >= data.endDate) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "Start date must be before end date",
        path: ["endDate"],
      });
    }

    // Participant count validation
    if (data.participants.length < 2) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "At least 2 participants required",
        path: ["participants"],
      });
    }

    // Duplicate validation
    const uniqueParticipants = new Set(data.participants);
    if (uniqueParticipants.size !== data.participants.length) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "Duplicate participants found",
        path: ["participants"],
      });
    }
  });
```

## safeParse (Safe Validation)

```typescript theme={null}
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  @api({ httpMethod: "POST" })
  async create(params: unknown): Promise<{ userId: number }> {
    // safeParse doesn't throw exceptions
    const result = CreateUserSchema.safeParse(params);

    if (!result.success) {
      // Validation failed
      const context = Sonamu.getContext();
      context.reply.status(400);

      const errors = result.error.errors.map((e) => ({
        field: e.path.join("."),
        message: e.message,
      }));

      throw new Error(JSON.stringify({ errors }));
    }

    // Validation succeeded
    const validated = result.data;

    const wdb = this.getPuri("w");
    const [user] = await wdb.table("users").insert(validated).returning("id");

    return { userId: user.id };
  }
}
```

## Customizing Error Messages

### Global Error Map

```typescript theme={null}
import { z } from "zod";

// Customize error messages
z.setErrorMap((issue, ctx) => {
  if (issue.code === z.ZodIssueCode.invalid_type) {
    if (issue.expected === "string") {
      return { message: "Must be a string" };
    }
    if (issue.expected === "number") {
      return { message: "Must be a number" };
    }
  }

  if (issue.code === z.ZodIssueCode.too_small) {
    if (issue.type === "string") {
      return { message: `Must be at least ${issue.minimum} characters` };
    }
  }

  return { message: ctx.defaultError };
});
```

### Individual Error Messages

```typescript theme={null}
const UserSchema = z.object({
  email: z
    .string({
      required_error: "Email is required",
      invalid_type_error: "Email must be a string",
    })
    .email("Invalid email format"),

  age: z
    .number({
      required_error: "Age is required",
      invalid_type_error: "Age must be a number",
    })
    .min(18, "Must be at least 18 years old"),
});
```

## Cautions

<Warning>
  **Cautions when using Zod validation**: 1. parse() throws exception on validation failure 2.
  safeParse() returns result object 3. Transform executes after validation 4. refine() executes last
  5\. Reuse schemas for performance
</Warning>

## Next Steps

<CardGroup cols={2}>
  <Card title="Custom Validation" icon="code" href="/en/api-development/validation/custom-validation">
    Implementing custom validation logic
  </Card>

  <Card title="Error Handling" icon="triangle-exclamation" href="/en/api-development/validation/error-handling">
    Using SonamuError
  </Card>

  <Card title="Parameters" icon="sliders" href="/en/api-development/creating-apis/parameters">
    API parameter definitions
  </Card>

  <Card title="@api Decorator" icon="at" href="/en/api-development/creating-apis/api-decorator">
    API basic usage
  </Card>
</CardGroup>
