Zod: chặn dữ liệu sai từ API của khách trước khi agent đọc nó
Ở site khách, API trả về sai một trường là đủ khiến agent ra quyết định sai. Một schema Zod đặt ở lối vào giúp bạn phát hiện lỗi đó trước khi agent đọc dữ liệu.
Tóm tắt nhanh
- Zod kiểm tra dữ liệu lúc chạy và suy ra kiểu TypeScript từ cùng một schema.
- safeParse trả về một object kết quả thay vì ném lỗi, hợp với payload thất thường của khách.
- Ai viết công cụ cho agent bằng SDK TypeScript của MCP sẽ phải dùng Zod, vì SDK này bắt buộc có nó.
Bạn đang viết một MCP server bằng TypeScript cho khách hàng? Vậy thì Zod đã có trong dự án trước cả khi bạn kịp chọn thư viện. Tài liệu chính thức của SDK TypeScript cho MCP ghi rõ SDK này cần zod làm peer dependency bắt buộc để kiểm tra schema.
Chi tiết này cho thấy cách người ta xây công cụ cho agent hiện nay: trước khi dữ liệu đi vào agent, nó phải đi qua một schema. Với FDE, lối vào quan trọng nhất là API của khách. Đó thường là hệ thống cũ, ít tài liệu, và đôi khi trả về những thứ không ai lường trước.
Lỗi kiểu này ít khi làm chương trình sập ngay. Nó lặng lẽ đi tiếp vào prompt, vào phép tính, vào quyết định của agent, rồi lộ ra sau ba ngày dưới dạng một câu trả lời sai mà không ai truy được nguồn. Zod giúp bạn chặn lỗi ở cửa thay vì đi tìm nó ở cuối đường ống.
Zod chỉ làm một việc, nhưng làm từ cả hai phía
Trang chính thức zod.dev mô tả Zod là thư viện kiểm tra schema viết cho TypeScript ngay từ đầu, có khả năng suy ra kiểu tĩnh. Thư viện do Colin McDonnell tạo ra, dùng giấy phép MIT và không phụ thuộc thư viện ngoài nào. API của Zod là immutable, còn phần lõi có kích thước rất nhỏ.
“Từ cả hai phía” nghĩa là thế này. Khi chạy, .parse kiểm tra input và trả về một bản sao sâu đã có kiểu, nên dữ liệu chưa kiểm tra chỉ trở thành dữ liệu có kiểu sau khi qua schema.
Khi biên dịch, z.infer suy ra kiểu TypeScript từ chính schema đó. Nhờ vậy bạn không còn phải giữ một interface viết tay và một hàm kiểm tra riêng, rồi lo hai thứ đó lệch nhau.
Một payload đơn hàng đi qua schema
Thử hình dung khách là một chuỗi bán lẻ, và agent của bạn đọc đơn hàng để trả lời nhân viên chăm sóc khách hàng. Tài liệu API ghi amount là số. Nhưng một ngày, response trả về { "id": "A-1029", "amount": "12.5", "status": "paid" }, tức amount là chuỗi.
import { z } from "zod";
const Order = z.object({
id: z.string(),
amount: z.number(),
status: z.string(),
});
type Order = z.infer<typeof Order>;
const result = Order.safeParse(rawFromCustomerApi);
if (!result.success) {
for (const issue of result.error.issues) {
console.warn(issue.path, issue.code, issue.message);
}
} else {
runAgent(result.data); // result.data có kiểu Order
}
Có hai lựa chọn ở đây. .parse sẽ ném ra ZodError khi input sai. safeParse thì không ném lỗi mà trả về một object kết quả, chứa dữ liệu đã parse hoặc một ZodError. Với dữ liệu từ khách, nơi lỗi là chuyện thường ngày chứ không phải ngoại lệ, safeParse thường là mặc định hợp lý hơn.
Thông tin trong ZodError khá chi tiết: mỗi vấn đề có code, path và message. Trong ví dụ trên, path sẽ trỏ đúng vào amount. Thay vì báo khách “agent trả lời sai”, bạn có thể nói “endpoint đơn hàng trả amount dạng chuỗi”, và đội của họ biết ngay phải sửa ở đâu.
Ba cái bẫy, và cách sửa ca amount là chuỗi
Bẫy phổ biến nhất là gọi .parse thẳng lên dữ liệu thô của khách trong vòng lặp của agent mà không bọc try/catch. Chỉ một payload sai là ZodError bị ném ra và cả lượt xử lý dừng lại. Ở lối vào này, safeParse an toàn hơn.
Cái bẫy kế tiếp là tự ép kiểu cho “chạy được” mà không hỏi khách. Đổi "12.5" thành 12.5 trông vô hại, nhưng bạn chưa biết đó là lỗi tạm thời, định dạng mới hay một đơn vị khác. Ép kiểu lặng lẽ là giấu đi đúng tín hiệu mà schema vừa bắt được.
Và đừng giữ song song một interface viết tay với một schema. Sớm muộn hai thứ sẽ lệch nhau; z.infer đưa chúng về một nguồn.
Với ca amount, có hai cách sửa đúng, tùy câu trả lời của khách. Nếu khách xác nhận amount phải là số, giữ nguyên z.number(): safeParse từ chối payload, bạn ghi lại path và chuyển lỗi cho đội của họ. Nếu khách xác nhận endpoint giờ trả chuỗi là có chủ ý, hãy ghi điều đó vào schema rồi chuyển đổi ở một chỗ rõ ràng:
const OrderRaw = z.object({
id: z.string(),
amount: z.string(), // khách xác nhận: số tiền trả về dạng chuỗi
status: z.string(),
});
const r = OrderRaw.safeParse(rawFromCustomerApi);
if (r.success) {
const amount = Number(r.data.amount);
if (Number.isNaN(amount)) {
// chuỗi không đọc được thành số: từ chối, không đưa cho agent
} else {
runAgent({ ...r.data, amount });
}
}
Khác biệt nằm ở chỗ việc chuyển đổi giờ là một quyết định đã thống nhất với khách, có comment và nằm ở một chỗ trong code, chứ không phải một bản vá lặng lẽ không ai biết.
Zod không giúp được bạn ở đâu?
Zod kiểm tra hình dạng dữ liệu, không kiểm tra ý nghĩa của nó. Nếu amount là số 1250 thì schema vẫn chấp nhận, dù khách tính bằng xu hay bằng đồng. Muốn biết điều đó, bạn phải ngồi với kỹ sư của khách.
Zod cũng không tự quyết thay bạn khi dữ liệu sai: từ chối, ghi log rồi bỏ qua, hay chuyển cho người xử lý. Đó là quyết định sản phẩm, nên bàn với khách từ sớm. Thêm một giới hạn hiển nhiên: Zod sống trong hệ sinh thái TypeScript, nên nếu pipeline của khách viết bằng ngôn ngữ khác, bạn chỉ dùng được Zod ở phần TypeScript.
Còn một chuyện về phiên bản. Zod 4 đã được công bố là bản ổn định. Release notes cho biết Zod 4 parse nhanh hơn Zod 3 tới 14.71 lần trong benchmark của họ, và phần lõi nhỏ hơn khoảng 57% (2,3 lần).
Nếu codebase của khách vẫn dùng Zod 3, hãy coi việc nâng cấp là một thay đổi riêng, có test riêng, đừng gộp nó vào một commit sửa lỗi.
Học gì trước, và thể hiện kỹ năng này thế nào?
Thứ tự hợp lý là z.object với vài kiểu cơ bản, rồi safeParse, rồi đọc ZodError, cuối cùng là z.infer. Bốn thứ đó đủ để bạn dựng một lớp kiểm tra ở lối vào cho hầu hết các tích hợp. Sau đó, hãy đọc cách SDK TypeScript của MCP dùng Zod để mô tả input của tool, vì đó chính là chỗ schema gặp agent.
Khi đọc mô tả công việc FDE, nếu tin tuyển dụng nhắc đến việc xây MCP server hay công cụ cho agent bằng TypeScript, hãy nêu kinh nghiệm Zod ngay trong thư ứng tuyển: SDK TypeScript của MCP bắt buộc phải có Zod, nên đây là kỹ năng bạn sẽ dùng từ tuần đầu.
Trong CV, đừng chỉ ghi “biết Zod”. Hãy viết theo kết quả, ví dụ: đặt schema ở lối vào cho các API của khách, gom lỗi theo trường, và rút ngắn thời gian tìm nguyên nhân khi agent trả lời sai.
Nhiều người nghĩ FDE giỏi là người viết prompt hay. Nhưng khi agent đi vào production ở site khách, thứ giữ cho nó ổn định thường là một schema chạy trước prompt.
Bài này có hữu ích không?
Cảm ơn bạn đã góp ý!