mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
c8fab497fd
Co-authored-by: Alan-TheGentleman <alan@thegentleman.dev> Co-authored-by: pedrooot <pedromarting3@gmail.com> Co-authored-by: Andoni A. <14891798+andoniaf@users.noreply.github.com>
217 lines
4.9 KiB
Markdown
217 lines
4.9 KiB
Markdown
---
|
|
name: zod-4
|
|
description: >
|
|
Zod 4 schema validation patterns.
|
|
Trigger: When creating or updating Zod v4 schemas for validation/parsing (forms, request payloads, adapters), including v3 -> v4 migration patterns.
|
|
license: Apache-2.0
|
|
metadata:
|
|
author: prowler-cloud
|
|
version: "1.0"
|
|
scope: [root, ui]
|
|
auto_invoke: "Creating Zod schemas"
|
|
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
|
|
---
|
|
|
|
## Breaking Changes from Zod 3
|
|
|
|
```typescript
|
|
// ❌ Zod 3 (OLD)
|
|
z.string().email()
|
|
z.string().uuid()
|
|
z.string().url()
|
|
z.string().nonempty()
|
|
z.object({ name: z.string() }).required_error("Required")
|
|
|
|
// ✅ Zod 4 (NEW)
|
|
z.email()
|
|
z.uuid()
|
|
z.url()
|
|
z.string().min(1)
|
|
z.object({ name: z.string() }, { error: "Required" })
|
|
```
|
|
|
|
## Basic Schemas
|
|
|
|
```typescript
|
|
import { z } from "zod";
|
|
|
|
// Primitives
|
|
const stringSchema = z.string();
|
|
const numberSchema = z.number();
|
|
const booleanSchema = z.boolean();
|
|
const dateSchema = z.date();
|
|
|
|
// Top-level validators (Zod 4)
|
|
const emailSchema = z.email();
|
|
const uuidSchema = z.uuid();
|
|
const urlSchema = z.url();
|
|
|
|
// With constraints
|
|
const nameSchema = z.string().min(1).max(100);
|
|
const ageSchema = z.number().int().positive().max(150);
|
|
const priceSchema = z.number().min(0).multipleOf(0.01);
|
|
```
|
|
|
|
## Object Schemas
|
|
|
|
```typescript
|
|
const userSchema = z.object({
|
|
id: z.uuid(),
|
|
email: z.email({ error: "Invalid email address" }),
|
|
name: z.string().min(1, { error: "Name is required" }),
|
|
age: z.number().int().positive().optional(),
|
|
role: z.enum(["admin", "user", "guest"]),
|
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
});
|
|
|
|
type User = z.infer<typeof userSchema>;
|
|
|
|
// Parsing
|
|
const user = userSchema.parse(data); // Throws on error
|
|
const result = userSchema.safeParse(data); // Returns { success, data/error }
|
|
|
|
if (result.success) {
|
|
console.log(result.data);
|
|
} else {
|
|
console.log(result.error.issues);
|
|
}
|
|
```
|
|
|
|
## Arrays and Records
|
|
|
|
```typescript
|
|
// Arrays
|
|
const tagsSchema = z.array(z.string()).min(1).max(10);
|
|
const numbersSchema = z.array(z.number()).nonempty();
|
|
|
|
// Records (objects with dynamic keys)
|
|
const scoresSchema = z.record(z.string(), z.number());
|
|
// { [key: string]: number }
|
|
|
|
// Tuples
|
|
const coordinatesSchema = z.tuple([z.number(), z.number()]);
|
|
// [number, number]
|
|
```
|
|
|
|
## Unions and Discriminated Unions
|
|
|
|
```typescript
|
|
// Simple union
|
|
const stringOrNumber = z.union([z.string(), z.number()]);
|
|
|
|
// Discriminated union (more efficient)
|
|
const resultSchema = z.discriminatedUnion("status", [
|
|
z.object({ status: z.literal("success"), data: z.unknown() }),
|
|
z.object({ status: z.literal("error"), error: z.string() }),
|
|
]);
|
|
```
|
|
|
|
## Transformations
|
|
|
|
```typescript
|
|
// Transform during parsing
|
|
const lowercaseEmail = z.email().transform(email => email.toLowerCase());
|
|
|
|
// Coercion (convert types)
|
|
const numberFromString = z.coerce.number(); // "42" → 42
|
|
const dateFromString = z.coerce.date(); // "2024-01-01" → Date
|
|
|
|
// Preprocessing
|
|
const trimmedString = z.preprocess(
|
|
val => typeof val === "string" ? val.trim() : val,
|
|
z.string()
|
|
);
|
|
```
|
|
|
|
## Refinements
|
|
|
|
```typescript
|
|
const passwordSchema = z.string()
|
|
.min(8)
|
|
.refine(val => /[A-Z]/.test(val), {
|
|
message: "Must contain uppercase letter",
|
|
})
|
|
.refine(val => /[0-9]/.test(val), {
|
|
message: "Must contain number",
|
|
});
|
|
|
|
// With superRefine for multiple errors
|
|
const formSchema = z.object({
|
|
password: z.string(),
|
|
confirmPassword: z.string(),
|
|
}).superRefine((data, ctx) => {
|
|
if (data.password !== data.confirmPassword) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "Passwords don't match",
|
|
path: ["confirmPassword"],
|
|
});
|
|
}
|
|
});
|
|
```
|
|
|
|
## Optional and Nullable
|
|
|
|
```typescript
|
|
// Optional (T | undefined)
|
|
z.string().optional()
|
|
|
|
// Nullable (T | null)
|
|
z.string().nullable()
|
|
|
|
// Both (T | null | undefined)
|
|
z.string().nullish()
|
|
|
|
// Default values
|
|
z.string().default("unknown")
|
|
z.number().default(() => Math.random())
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
```typescript
|
|
// Zod 4: Use 'error' param instead of 'message'
|
|
const schema = z.object({
|
|
name: z.string({ error: "Name must be a string" }),
|
|
email: z.email({ error: "Invalid email format" }),
|
|
age: z.number().min(18, { error: "Must be 18 or older" }),
|
|
});
|
|
|
|
// Custom error map
|
|
const customSchema = z.string({
|
|
error: (issue) => {
|
|
if (issue.code === "too_small") {
|
|
return "String is too short";
|
|
}
|
|
return "Invalid string";
|
|
},
|
|
});
|
|
```
|
|
|
|
## React Hook Form Integration
|
|
|
|
```typescript
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
|
|
const schema = z.object({
|
|
email: z.email(),
|
|
password: z.string().min(8),
|
|
});
|
|
|
|
type FormData = z.infer<typeof schema>;
|
|
|
|
function Form() {
|
|
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
|
|
resolver: zodResolver(schema),
|
|
});
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit(onSubmit)}>
|
|
<input {...register("email")} />
|
|
{errors.email && <span>{errors.email.message}</span>}
|
|
</form>
|
|
);
|
|
}
|
|
```
|