# Zod là gì? Hướng dẫn validate với Zod và ví dụ

## 1. Zod là gì và tại sao nên sử dụng?
Xin chào các bạn! Hôm nay tôi muốn chia sẻ về Zod - thư viện validate đã thay đổi hoàn toàn cách tôi phát triển ứng dụng TypeScript.
Trước đây tôi đã sử dụng Yup cho tất cả dự án của mình. Yup là một thư viện tốt, nhưng khi làm việc nhiều với TypeScript, tôi luôn phải định nghĩa schema và type riêng biệt, dẫn đến code dài dòng và dễ sai sót. Chưa kể khả năng type inference của Yup còn hạn chế, không phản ánh đúng các ràng buộc runtime.
**Zod** đã giải quyết tất cả vấn đề đó với thiết kế "TypeScript-first", cho phép bạn vừa định nghĩa validation schema vừa tự động sinh ra type definitions.
## 2. So sánh Zod và Yup
### 2.1. Bảng so sánh chi tiết
| Tiêu chí | Zod | Yup |
|----------|-----|-----|
| **TypeScript integration** | ⭐⭐⭐⭐⭐ <br> Được thiết kế từ đầu cho TypeScript, tự động suy luận type | ⭐⭐⭐ <br> Hỗ trợ TypeScript nhưng yêu cầu thêm type definitions thủ công |
| **Kích thước (bundled)** | ~11KB | ~24KB |
| **Hiệu suất** | ⭐⭐⭐⭐⭐ <br> Nhanh hơn 25-35% so với Yup | ⭐⭐⭐ <br> Tốt nhưng chậm hơn khi xử lý schema phức tạp |
| **Cú pháp** | Thiên về methods chaining <br> `z.string().email().min(5)` | Thiên về object config <br> `yup.string().email().min(5)` |
| **Custom validations** | `refine()` và `superRefine()` mạnh mẽ | Chỉ có `test()` |
| **Transformations** | `transform()`, `catch()`, `default()` | `transform()`, `default()` |
| **Async validations** | ✅ Hỗ trợ tốt | ✅ Hỗ trợ tốt và đã lâu đời hơn |
| **Dependencies** | Zero dependencies | Có một số dependencies |
| **Learning curve** | Khó học hơn ban đầu | Dễ học hơn cho người mới |
| **Community/Ecosystem** | Mới hơn, đang phát triển mạnh | Lâu đời hơn, stable và phổ biến |
| **Tính năng đặc biệt** | `z.infer<typeof schema>`, Recursive schemas, Branded types | Chưa có tương đương |
### 2.2. Biểu đồ Google Trends - Sự tăng trưởng của Zod

Bạn có thể tham khảo ở link này 👉: https://trends.google.com.vn/trends/explore?q=Zod%20npm,Yup%20npm&hl=vi
Ngay cả trên npm bây giờ Zod cũng đã vượt trội rất nhiều so với giúp ở **lượt tải mỗi tuần**
**Zod**:

**Yup**:

## 3. Các phương thức phổ biến của Zod
### 3.1. Bảng các phương thức Zod cơ bản
| Phương thức | Mô tả | Ví dụ |
|-------------|-------|-------|
| `z.string()` | Validate chuỗi | `z.string().min(5, "Tối thiểu 5 ký tự")` |
| `z.number()` | Validate số | `z.number().positive("Phải là số dương")` |
| `z.boolean()` | Validate boolean | `z.boolean()` |
| `z.date()` | Validate ngày tháng | `z.date().min(new Date(), "Phải là ngày trong tương lai")` |
| `z.array()` | Validate mảng | `z.array(z.string()).nonempty("Cần ít nhất 1 phần tử")` |
| `z.object()` | Validate đối tượng | `z.object({ name: z.string(), age: z.number() })` |
| `z.union()` | Validate nhiều loại | `z.union([z.string(), z.number()])` |
| `z.tuple()` | Validate mảng cố định | `z.tuple([z.string(), z.number(), z.boolean()])` |
| `z.enum()` | Validate giá trị từ enum | `z.enum(["apple", "banana", "orange"])` |
| `z.literal()` | Validate giá trị cụ thể | `z.literal("hello")` |
| `z.nullable()` | Cho phép null | `z.string().nullable()` |
| `z.optional()` | Cho phép undefined | `z.string().optional()` |
| `z.record()` | Validate key-value | `z.record(z.string())` |
| `z.map()` | Validate Map object | `z.map(z.string(), z.number())` |
| `z.set()` | Validate Set object | `z.set(z.string())` |
| `z.lazy()` | Cho phép recursive schema | `z.lazy(() => UserSchema)` |
### 3.2. Bảng các phương thức validate chuỗi
| Phương thức | Mô tả | Ví dụ |
|-------------|-------|-------|
| `min()` | Độ dài tối thiểu | `z.string().min(8, "Tối thiểu 8 ký tự")` |
| `max()` | Độ dài tối đa | `z.string().max(100, "Tối đa 100 ký tự")` |
| `length()` | Độ dài cố định | `z.string().length(10, "Phải có 10 ký tự")` |
| `email()` | Định dạng email | `z.string().email("Email không hợp lệ")` |
| `url()` | Định dạng URL | `z.string().url("URL không hợp lệ")` |
| `uuid()` | Định dạng UUID | `z.string().uuid("UUID không hợp lệ")` |
| `regex()` | Kiểm tra theo regex | `z.string().regex(/^[A-Z]+$/, "Chỉ chấp nhận chữ hoa")` |
| `includes()` | Phải chứa chuỗi con | `z.string().includes("abc", "Phải chứa 'abc'")` |
| `startsWith()` | Phải bắt đầu bằng | `z.string().startsWith("https://", "Phải bắt đầu bằng https://")` |
| `endsWith()` | Phải kết thúc bằng | `z.string().endsWith(".com", "Phải kết thúc bằng .com")` |
| `trim()` | Cắt khoảng trắng | `z.string().trim()` |
| `toLowerCase()` | Chuyển thành chữ thường | `z.string().toLowerCase()` |
| `toUpperCase()` | Chuyển thành chữ hoa | `z.string().toUpperCase()` |
### 3.3. Bảng các phương thức validate số
| Phương thức | Mô tả | Ví dụ |
|-------------|-------|-------|
| `min()` | Giá trị tối thiểu | `z.number().min(18, "Phải từ 18 tuổi trở lên")` |
| `max()` | Giá trị tối đa | `z.number().max(100, "Tối đa 100")` |
| `int()` | Phải là số nguyên | `z.number().int("Phải là số nguyên")` |
| `positive()` | Phải là số dương | `z.number().positive("Phải là số dương")` |
| `negative()` | Phải là số âm | `z.number().negative("Phải là số âm")` |
| `nonpositive()` | Phải ≤ 0 | `z.number().nonpositive("Phải nhỏ hơn hoặc bằng 0")` |
| `nonnegative()` | Phải ≥ 0 | `z.number().nonnegative("Phải lớn hơn hoặc bằng 0")` |
| `multipleOf()` | Phải là bội số | `z.number().multipleOf(5, "Phải là bội số của 5")` |
| `finite()` | Phải hữu hạn | `z.number().finite("Không thể là vô cùng")` |
| `safe()` | Phải là số an toàn | `z.number().safe("Phải là số an toàn trong JavaScript")` |
### 3.4. Bảng các phương thức biến đổi và tinh chỉnh
| Phương thức | Mô tả | Ví dụ |
|-------------|-------|-------|
| `transform()` | Biến đổi giá trị | `z.string().transform(val => val.toUpperCase())` |
| `default()` | Đặt giá trị mặc định | `z.string().default("guest")` |
| `catch()` | Đặt giá trị nếu lỗi | `z.number().catch(0)` |
| `refine()` | Validate tùy chỉnh | `z.string().refine(val => val.length % 2 === 0, "Phải có số ký tự chẵn")` |
| `superRefine()` | Validate tùy chỉnh nâng cao | `z.object({...}).superRefine((data, ctx) => {...})` |
| `pipe()` | Kết hợp schemas | `z.string().pipe(z.number())` |
| `preprocess()` | Xử lý trước khi validate | `z.preprocess(val => String(val), z.string())` |
## 4. Cài đặt và thiết lập cơ bản
```bash
# Với NPM
npm install zod
# Với YARN
yarn add zod
# Với PNPM
pnpm add zod
```
Khi sử dụng với React Hook Form:
```bash
npm install react-hook-form @hookform/resolvers
```
## 5. Hiểu rõ về Refine và SuperRefine
Zod nổi bật với hai tính năng mạnh mẽ: `refine()` và `superRefine()`. Đây là những gì đã khiến tôi chuyển từ Yup sang Zod.
### 5.1. Phương thức `refine()`
`refine()` cho phép bạn thêm logic validation tùy chỉnh cho schema, giúp xử lý các trường hợp mà các validators tiêu chuẩn không đáp ứng được.
```typescript
const passwordSchema = z.string()
.min(8, "Mật khẩu phải có ít nhất 8 ký tự")
.refine(
(password) => /[A-Z]/.test(password),
{ message: "Mật khẩu phải có ít nhất 1 chữ cái viết hoa" }
);
```
Yup cũng có `test()` nhưng cú pháp phức tạp hơn:
```typescript
const passwordSchema = yup.string()
.min(8, "Mật khẩu phải có ít nhất 8 ký tự")
.test(
'has-uppercase',
"Mật khẩu phải có ít nhất 1 chữ cái viết hoa",
(value) => /[A-Z]/.test(value || '')
);
```
### 5.2. Phương thức `superRefine()` - Bí quyết validate tuần tự
`superRefine()` là **"vũ khí bí mật"** của Zod mà Yup không có, cho phép:
- Truy cập sâu hơn vào context validation
- Validate các trường dữ liệu một cách tuần tự
- Kiểm soát chi tiết thông báo lỗi và path
Đây là ví dụ so sánh:
#### 5.2.1. Validate với Yup (tất cả lỗi hiển thị cùng lúc)
```typescript
// Với Yup - tất cả lỗi hiển thị đồng thời
const schema = yup.object({
email: yup.string().required("Email không được để trống").email("Email không hợp lệ"),
password: yup.string().required("Mật khẩu không được để trống"),
confirmPassword: yup.string().required("Vui lòng xác nhận mật khẩu")
.oneOf([yup.ref('password')], "Mật khẩu không khớp")
});
```
#### 5.2.2. Validate tuần tự với Zod (sử dụng superRefine)
```typescript
// Với Zod - validate tuần tự
const schema = z
.object({
email: z.string(),
password: z.string(),
confirmPassword: z.string()
})
// Bước 1: Validate email
.superRefine((data, ctx) => {
if (!data.email) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["email"],
message: "Email không được để trống"
});
return; // Dừng validation nếu email trống
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["email"],
message: "Email không hợp lệ"
});
return;
}
})
// Bước 2: Chỉ validate password khi email hợp lệ
.superRefine((data, ctx) => {
// Bỏ qua nếu email không hợp lệ
if (!data.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
return;
}
if (!data.password) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["password"],
message: "Mật khẩu không được để trống"
});
return;
}
// Kiểm tra độ mạnh mật khẩu...
})
// Bước 3: Chỉ validate confirmPassword khi password hợp lệ
.superRefine((data, ctx) => {
// Điều kiện bỏ qua tương tự...
if (!data.confirmPassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["confirmPassword"],
message: "Vui lòng xác nhận mật khẩu"
});
return;
}
if (data.password !== data.confirmPassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["confirmPassword"],
message: "Mật khẩu không khớp"
});
}
});
```
## 6. Ví dụ thực tế: Form đăng ký với validate tuần tự
Dưới đây là ví dụ hoàn chỉnh về form đăng ký sử dụng Zod với React Hook Form, tập trung vào validate tuần tự:
Bạn hãy vào link này để trãi nghiệm: https://codesandbox.io/p/devbox/pfnvdf
```tsx=
// Định nghĩa các yêu cầu cho mật khẩu
const passwordRequirements = {
minLength: 8,
hasUppercase: /[A-Z]/,
hasLowercase: /[a-z]/,
hasNumber: /\d/,
hasSpecialChar: /[!@#$%^&*()_+\-=[\]{}|;:'",.<>/?\\~`]/,
};
// Schema validate form đăng ký sử dụng superRefine để validate tuần tự
const signUpSchema = z
.object({
email: z.string(),
password: z.string(),
confirmPassword: z.string(),
fullName: z.string(),
terms: z.boolean(),
})
// Bước 1: Validate email
.superRefine((data, ctx) => {
// Kiểm tra email trống
if (!data.email) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Email không được để trống",
path: ["email"],
});
return; // Dừng lại nếu email trống
}
// Kiểm tra định dạng email
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Email không đúng định dạng",
path: ["email"],
});
return;
}
})
// Bước 2: Validate họ tên - chỉ khi email hợp lệ
.superRefine((data, ctx) => {
// Bỏ qua nếu email chưa hợp lệ
if (!data.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
return;
}
if (!data.fullName) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Họ tên không được để trống",
path: ["fullName"],
});
return;
}
if (data.fullName.length < 2) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Họ tên phải có ít nhất 2 ký tự",
path: ["fullName"],
});
return;
}
})
// Bước 3: Validate mật khẩu - chỉ khi họ tên hợp lệ
.superRefine((data, ctx) => {
// Bỏ qua nếu email hoặc họ tên chưa hợp lệ
if (
!data.email ||
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email) ||
!data.fullName ||
data.fullName.length < 2
) {
return;
}
// Kiểm tra mật khẩu trống - SỬA LỖI Ở ĐÂY
// Thêm trim() để tránh trường hợp chỉ có khoảng trắng và kiểm tra có giá trị thực sự
if (!data.password || data.password.trim() === "") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Mật khẩu không được để trống",
path: ["password"],
});
return;
}
// Kiểm tra độ dài mật khẩu
if (data.password.length < passwordRequirements.minLength) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Mật khẩu phải có ít nhất ${passwordRequirements.minLength} ký tự`,
path: ["password"],
});
return;
}
// Kiểm tra chữ hoa
if (!passwordRequirements.hasUppercase.test(data.password)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Mật khẩu phải có ít nhất 1 chữ cái viết hoa",
path: ["password"],
});
return;
}
// Kiểm tra chữ thường
if (!passwordRequirements.hasLowercase.test(data.password)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Mật khẩu phải có ít nhất 1 chữ cái viết thường",
path: ["password"],
});
return;
}
// Kiểm tra số
if (!passwordRequirements.hasNumber.test(data.password)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Mật khẩu phải có ít nhất 1 số",
path: ["password"],
});
return;
}
// Kiểm tra ký tự đặc biệt
if (!passwordRequirements.hasSpecialChar.test(data.password)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Mật khẩu phải có ít nhất 1 ký tự đặc biệt",
path: ["password"],
});
return;
}
})
// Bước 4: Validate xác nhận mật khẩu - chỉ khi mật khẩu hợp lệ
.superRefine((data, ctx) => {
// Điều kiện bỏ qua kiểm tra (tất cả các bước trước phải hợp lệ)
if (
!data.email ||
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email) ||
!data.fullName ||
data.fullName.length < 2 ||
!data.password ||
data.password.length < passwordRequirements.minLength ||
!passwordRequirements.hasUppercase.test(data.password) ||
!passwordRequirements.hasLowercase.test(data.password) ||
!passwordRequirements.hasNumber.test(data.password) ||
!passwordRequirements.hasSpecialChar.test(data.password)
) {
return;
}
// Kiểm tra xác nhận mật khẩu trống
if (!data.confirmPassword || data.confirmPassword.trim() === "") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Vui lòng xác nhận mật khẩu",
path: ["confirmPassword"],
});
return;
}
// Kiểm tra mật khẩu khớp
if (data.password !== data.confirmPassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Mật khẩu không khớp",
path: ["confirmPassword"],
});
return;
}
})
// Bước 5: Validate điều khoản - chỉ khi tất cả các trường khác hợp lệ
.superRefine((data, ctx) => {
// Điều kiện bỏ qua (tất cả các trường trước phải hợp lệ)
if (
!data.email ||
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email) ||
!data.fullName ||
data.fullName.length < 2 ||
!data.password ||
data.password.length < passwordRequirements.minLength ||
!passwordRequirements.hasUppercase.test(data.password) ||
!passwordRequirements.hasLowercase.test(data.password) ||
!passwordRequirements.hasNumber.test(data.password) ||
!passwordRequirements.hasSpecialChar.test(data.password) ||
!data.confirmPassword ||
data.password !== data.confirmPassword
) {
return;
}
// Kiểm tra đồng ý điều khoản
if (!data.terms) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Bạn phải đồng ý với điều khoản dịch vụ",
path: ["terms"],
});
return;
}
});
```
### So sánh trải nghiệm người dùng
**Validation thông thường (tất cả lỗi cùng lúc):**

**Validation tuần tự với superRefine:**

## 7. Zod vs Yup: Nên chọn gì?
### 7.1. Nên chọn Zod khi:
- Bạn đang phát triển ứng dụng TypeScript và muốn tận dụng type safety
- Cần hiệu suất cao cho validation phức tạp
- Muốn tái sử dụng schema cho cả client và server validation
- Cần xử lý validation phức tạp theo tuần tự với superRefine
- Ưu tiên kích thước bundle nhỏ
### 7.2. Nên chọn Yup khi:
- Dự án đã có sẵn Yup và không cần type inference mạnh mẽ
- Làm việc trong môi trường JavaScript thuần
- Ưu tiên API đơn giản, dễ học
- Cần tính ổn định của thư viện đã phát triển lâu năm
## 8. Tổng kết
Zod đã trở thành thư viện validation ưa thích của tôi vì:
- **Tích hợp TypeScript tuyệt vời** - Không cần định nghĩa type riêng
- **Hiệu suất vượt trội** so với các giải pháp trước đây
- **superRefine** - Validate tuần tự cải thiện UX đáng kể
- **Zero dependencies** - Không lo về vấn đề bảo mật từ các dependencies
Nếu bạn đang sử dụng TypeScript, tôi khuyến khích thử Zod thay vì Yup. Với những cải tiến liên tục và hiệu suất vượt trội, Zod đang là xu hướng validation framework của tương lai.
## 9. Tài liệu tham khảo
- [Tài liệu chính thức của Zod](https://zod.dev)
- [GitHub Repository](https://github.com/colinhacks/zod)
- [Ví dụ đầy đủ về form đăng ký](https://codesandbox.io/p/devbox/pfnvdf)
- [So sánh hiệu suất Zod vs Yup](https://github.com/colinhacks/zod#comparison)