Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

JSON Schema Validation trong tests

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn là QA tại một startup fintech ở TP.HCM. Đội backend deploy một bản vá nhỏ vào 2 giờ sáng, và trong bản đó họ đổi field balance từ kiểu number sang string (vì "để tránh sai số làm tròn"). Test của bạn có 15 assertion, nhưng không assertion nào bắt được thay đổi này — vì bạn chỉ kiểm tra pm.expect(res.balance).to.exist. Field vẫn tồn tại, chỉ là kiểu dữ liệu đã đổi. App mobile parse balance như một số, giờ nhận về chuỗi "1500000", và màn hình số dư hiển thị NaN. Người dùng hoảng loạn, tổng đài quá tải, và bạn thì tự hỏi tại sao 15 assertion "xanh lè" lại không cứu được mình.

Đây chính xác là khoảng trống mà JSON Schema Validation lấp đầy. Ở các bài trước bạn đã học viết assertion thủ công cho từng field. Cách đó tốt cho những kiểm tra nghiệp vụ cụ thể (số dư phải > 0, trạng thái phải là active...). Nhưng nó không bảo vệ được hợp đồng cấu trúc (contract) của response: response phải có đúng những field nào, mỗi field kiểu gì, field nào bắt buộc, field nào được phép null. JSON Schema cho phép bạn khai báo một lần "response phải trông như thế này", rồi để một validator so khớp toàn bộ response với bản mô tả đó. Khi backend lặng lẽ đổi kiểu, thiếu field, hay thêm field lạ, test đỏ ngay lập tức — trước khi lỗi kịp chạm tới người dùng.

Bài này tập trung riêng vào việc validate response bằng JSON Schema bên trong test script của Postman. Chúng ta không bàn về assertion nghiệp vụ chung (đã học ở bài Tests Scripts), cũng không bàn về contract testing với OpenAPI (sẽ có ở bài riêng). Ở đây, mục tiêu là: bạn viết được schema, gắn nó vào test, và hiểu tại sao đây là "lưới an toàn" mạnh nhất cho các API đang thay đổi liên tục.

Khái niệm cốt lõi

JSON Schema là gì

JSON Schema là một chuẩn (bản thân nó cũng viết bằng JSON) dùng để mô tả cấu trúc của một tài liệu JSON. Nó trả lời các câu hỏi: object này có những property nào, mỗi property kiểu gì (string, number, boolean, object, array, null), property nào là bắt buộc (required), array chứa phần tử kiểu gì, giá trị có ràng buộc gì (min, max, pattern, enum...).

Ví dụ một schema mô tả một user đơn giản:

const userSchema = {
  type: "object",
  required: ["id", "email", "is_active"],
  properties: {
    id: { type: "integer" },
    email: { type: "string" },
    is_active: { type: "boolean" },
    phone: { type: "string" }   // không nằm trong required => tùy chọn
  }
};

Ý nghĩa: response phải là một object, bắt buộcid, email, is_active. id phải là số nguyên, email phải là chuỗi, is_active phải là boolean. phone có thể có hoặc không, nhưng nếu có thì phải là chuỗi.

Hai công cụ validate trong Postman: tv4 và Ajv

Postman cung cấp sẵn hai thư viện validate schema, không cần cài đặt gì thêm:

tv4 (tiny validator for JSON Schema v4) — nhỏ gọn, có sẵn qua biến toàn cục tv4. Cú pháp cực đơn giản:

const schema = { / ... / };
pm.test("Response khớp schema (tv4)", function () {
    const data = pm.response.json();
    pm.expect(tv4.validate(data, schema)).to.be.true;
});

tv4 phù hợp cho schema đơn giản và khi bạn cần thứ gì đó chạy ngay. Nhược điểm: nó chỉ hỗ trợ JSON Schema draft-04 (bản cũ), thông báo lỗi khá sơ sài, và không được cập nhật nữa.

Ajv (Another JSON Validator) — mạnh hơn nhiều, hỗ trợ các draft mới (draft-07 trở lên), thông báo lỗi chi tiết, hiệu năng cao. Đây là công cụ được khuyến nghị cho các dự án nghiêm túc. Trong Postman bạn dùng qua pm.response.to.have.jsonSchema() (chạy nền bằng Ajv) hoặc require('ajv') để dùng trực tiếp:

pm.test("Response khớp schema (Ajv)", function () {
    pm.response.to.have.jsonSchema(schema);
});

Đây là cách viết ngắn gọn và "Postman-native" nhất. Khi assertion fail, Postman sẽ chỉ ra chính xác field nào sai.

Vì sao lấy schema chính xác lại quan trọng

Điểm mấu chốt của schema validation là bạn đang định nghĩa hợp đồng. Nếu schema quá lỏng (chỉ khai báo vài field, không đánh dấu required), test sẽ "xanh" ngay cả khi response hỏng — bạn có cảm giác an toàn giả tạo. Nếu schema quá chặt (khai báo mọi field là required, cấm mọi field lạ bằng additionalProperties: false), test sẽ đỏ liên tục mỗi khi backend thêm một field mới hoàn toàn hợp lệ, gây "mệt mỏi vì báo động giả" (alert fatigue) và cuối cùng đội của bạn sẽ tắt nó đi.

Nghệ thuật ở đây là cân bằng: khai báo required cho những field mà client thực sự phụ thuộc vào, định kiểu chặt chẽ, nhưng cho phép thêm field mới trừ khi có lý do nghiệp vụ cấm.

Tình huống thực tế

Tình huống 1 — Tiki và cú đổi kiểu ngầm

Một đội QA làm cho hệ thống giống Tiki có endpoint GET /api/v2/products/{id}. Response trả về giá sản phẩm ở field price. Ban đầu backend trả price: 250000 (number). Sau một sprint, đội backend tích hợp với một service mới trả giá dưới dạng chuỗi có định dạng, và price trở thành "250,000" (string, có dấu phẩy).

Test cũ chỉ có pm.expect(res.price).to.exist — vẫn pass. Kết quả: web frontend nhân price với số lượng để tính tổng, "250,000" * 2 cho ra NaN, giỏ hàng hiển thị "Tổng: NaN đ" trên production suốt 40 phút.

Nếu đội đã dùng schema:

const productSchema = {
    type: "object",
    required: ["id", "name", "price", "in_stock"],
    properties: {
        id: { type: "integer" },
        name: { type: "string" },
        price: { type: "number" },        // chốt cứng là số
        in_stock: { type: "boolean" }
    }
};
pm.test("Product schema hợp lệ", function () {
    pm.response.to.have.jsonSchema(productSchema);
});

Ngay khi price biến thành string, assertion fail với thông báo kiểu "data.price should be number". Test trong CI đỏ, PR bị chặn, lỗi không bao giờ ra tới production. Bài học: assertion to.exist chỉ kiểm tra sự tồn tại, còn schema kiểm tra cả kiểu — và đổi kiểu ngầm là một trong những lỗi backend phổ biến, khó phát hiện nhất.

Tình huống 2 — Startup fintech và field bị mất trong danh sách giao dịch

Một startup ví điện tử ở Hà Nội có endpoint GET /api/transactions trả về một mảng giao dịch. App mobile hiển thị mỗi dòng gồm amount, type, created_at. Sau một lần refactor, backend vô tình bỏ sót created_at khi map dữ liệu từ bảng mới. Test cũ kiểm tra res[0].amountres[0].type — vẫn pass vì hai field kia còn nguyên.

Với schema validate cả từng phần tử trong mảng:

const txListSchema = {
    type: "array",
    items: {
        type: "object",
        required: ["id", "amount", "type", "created_at"],
        properties: {
            id: { type: "string" },
            amount: { type: "number" },
            type: { type: "string", enum: ["credit", "debit"] },
            created_at: { type: "string" }
        }
    }
};
pm.test("Danh sách giao dịch đúng cấu trúc", function () {
    pm.response.to.have.jsonSchema(txListSchema);
});

Điểm hay: items áp schema cho mọi phần tử của mảng, nên dù mảng có 500 giao dịch, chỉ cần một giao dịch thiếu created_at là fail. Thêm nữa, enum: ["credit", "debit"] bắt luôn trường hợp backend trả một type lạ như "CREDIT" (viết hoa) hay "transfer" (giá trị mới chưa được app hỗ trợ). Bài học: với response dạng mảng, luôn validate cấu trúc của phần tử qua items, và dùng enum để khóa những field có tập giá trị cố định.

Tình huống 3 — Báo động giả vì schema quá chặt

Một đội ở công ty gia công phần mềm tại Đà Nẵng làm test cho API của khách hàng nước ngoài. Họ đặt additionalProperties: false cho mọi schema để "tuyệt đối nghiêm ngặt". Vấn đề: khách hàng liên tục thêm field mới không phá vỡ gì (thêm discount_label, badge, tracking_id...). Mỗi lần thêm, hàng chục test đỏ, đội phải sửa schema, khách phàn nàn "sao thêm field cũng làm test đỏ". Sau ba tuần, đội mất niềm tin và tắt toàn bộ schema validation — quay lại tình trạng còn tệ hơn ban đầu.

Cách xử lý đúng: bỏ additionalProperties: false mặc định, chỉ dùng nó ở những endpoint mà việc thêm field lạ thực sự là dấu hiệu bug (ví dụ endpoint trả secret, không được lộ thêm field). Ở các endpoint thông thường, cho phép field mới, chỉ chốt required + kiểu của những field mà client dùng. Bài học: schema validation chỉ có giá trị khi đội tin tưởng và giữ nó bật. Một schema quá nghiêm khắc bị tắt đi còn tệ hơn một schema vừa phải được duy trì.

Hướng dẫn từng bước

Giả sử bạn cần thêm schema validation cho endpoint GET /api/orders/{id}.

Bước 1 — Lấy một response thật. Gửi request, nhìn kỹ response body. Đây là "hình mẫu" để bạn dựng schema. Đừng đoán, hãy lấy dữ liệu thực từ môi trường staging.

Bước 2 — Sinh schema nhanh. Bạn có thể viết tay, hoặc dùng công cụ online (search "JSON to JSON Schema generator") để sinh khung schema từ một response mẫu, rồi tinh chỉnh. Cách này tiết kiệm thời gian với response lớn. Nhớ: công cụ chỉ cho bạn bản nháp, bạn phải tự quyết field nào required, kiểu nào chặt.

Bước 3 — Đặt schema ở nơi tái sử dụng được. Đừng dán schema vào từng test. Lưu nó vào một biến environment hoặc collection variable dưới dạng chuỗi, rồi parse ra:

// Trong Pre-request Script hoặc ngay đầu Test
const orderSchema = JSON.parse(pm.collectionVariables.get("orderSchema"));

Với schema hay dùng chung nhiều request, bạn có thể lưu ở collection-level và nạp một lần. (Kỹ thuật tái sử dụng code sâu hơn nằm ở bài "Custom Functions & Code Reuse".)

Bước 4 — Viết test. Dùng cách Ajv-native cho thông báo lỗi tốt nhất:

pm.test("Order response khớp schema", function () {
    const schema = JSON.parse(pm.collectionVariables.get("orderSchema"));
    pm.response.to.have.jsonSchema(schema);
});

Bước 5 — Cố tình phá để kiểm chứng. Đây là bước nhiều người bỏ qua nhưng cực kỳ quan trọng. Tạm sửa schema (đổi một field thành kiểu sai, thêm một field required không tồn tại) và chạy lại — test phải đỏ. Nếu vẫn xanh, nghĩa là schema của bạn quá lỏng và đang cho cảm giác an toàn giả. Sau khi xác nhận nó bắt được lỗi, khôi phục schema.

Bước 6 — Ràng buộc thêm khi cần. Với những field nghiệp vụ quan trọng, thêm ràng buộc giá trị ngay trong schema:

{
    status: { type: "string", enum: ["pending", "paid", "shipped", "cancelled"] },
    total: { type: "number", minimum: 0 },
    email: { type: "string", format: "email" },
    order_code: { type: "string", pattern: "^ORD-[0-9]{8}$" }
}

enum khóa tập giá trị, minimum chặn số âm, format: "email"pattern (regex) kiểm tra định dạng chuỗi. Nhờ vậy schema vừa kiểm cấu trúc vừa gánh một phần kiểm nghiệp vụ.

Lỗi thường gặp & mẹo

Quên required là lỗi số một. Nếu bạn chỉ liệt kê field trong properties mà không đưa vào required, thì field đó là tùy chọn — response thiếu hẳn field vẫn pass. Rất nhiều người tưởng "đã khai báo trong properties là bắt buộc". Không phải. properties chỉ nói "nếu field này có mặt thì phải đúng kiểu này". Muốn bắt buộc, phải thêm tên field vào mảng required.

Xử lý field có thể null. API thực tế thường trả null cho field chưa có giá trị (ví dụ deleted_at: null). Nếu schema khai type: "string", response null sẽ fail. Cho phép null bằng mảng kiểu:

deleted_at: { type: ["string", "null"] }

Nhầm giữa integernumber. integer chỉ chấp nhận số nguyên; number chấp nhận cả số thập phân. Nếu bạn khai id: { type: "integer" } nhưng backend đôi khi trả id dạng chuỗi số "42", nó sẽ fail — điều này thường là tốt (bắt được sự thiếu nhất quán), nhưng bạn cần biết mình đang chốt gì.

Đừng viết một schema khổng lồ cho mọi thứ. Response lồng nhau sâu nên được chia thành các schema con và tham chiếu, hoặc ít nhất tách theo endpoint. Một schema 300 dòng cho một response phức tạp sẽ không ai dám sửa.

Mẹo — dùng thông báo lỗi của Ajv để debug. Khi cần chi tiết, dùng Ajv trực tiếp và in ra lỗi:

const Ajv = require('ajv');
const ajv = new Ajv({ allErrors: true });
const validate = ajv.compile(schema);
const valid = validate(pm.response.json());
pm.test("Schema hợp lệ", function () {
    if (!valid) console.log(validate.errors);  // xem chính xác field nào sai
    pm.expect(valid, JSON.stringify(validate.errors)).to.be.true;
});

allErrors: true cho bạn thấy tất cả lỗi chứ không dừng ở lỗi đầu tiên — rất hữu ích khi response sai nhiều chỗ.

Mẹo — schema là tài liệu sống. Một schema tốt còn đóng vai trò tài liệu: người mới nhìn schema là biết response endpoint đó trông ra sao, field nào bắt buộc. Hãy giữ nó cập nhật cùng API.

Bài tập thực hành

  • Dựng schema đầu tiên. Chọn một API công khai bất kỳ (ví dụ https://reqres.in/api/users/2). Lấy response, viết schema đầy đủ với type, properties, và required cho các field quan trọng. Dùng pm.response.to.have.jsonSchema() để validate. Chạy và xác nhận test xanh.
  • Kiểm chứng bằng cách phá. Trên chính test ở bài 1: đổi kiểu một field trong schema thành kiểu sai (ví dụ id thành boolean), chạy lại và xác nhận test đỏ. Đọc kỹ thông báo lỗi. Sau đó khôi phục.
  • Validate mảng. Tìm một endpoint trả về mảng (ví dụ https://reqres.in/api/users). Viết schema với type: "array"items mô tả từng phần tử, có required. Thêm một ràng buộc enum hoặc minimum cho ít nhất một field.
  • Xử lý null và optional. Tạo một schema trong đó một field cho phép ["string", "null"] và một field khác là optional (không nằm trong required). Tự tạo hai response giả (một có null, một thiếu field optional) và xác nhận cả hai đều pass, còn response thiếu field required thì fail.
  • Tình huống fintech. Viết schema cho response giao dịch gồm id (string), amount (number, minimum 0), type (enum credit/debit), created_at (string), deleted_at (string hoặc null). So sánh cảm giác an toàn khi có schema với khi chỉ dùng to.exist.

Tóm tắt

JSON Schema Validation là lưới an toàn mạnh nhất để bảo vệ hợp đồng cấu trúc của API — thứ mà assertion to.exist thủ công không thể bắt được. Bằng cách khai báo một lần response "phải trông thế nào" (field nào required, kiểu gì, ràng buộc gì), bạn phát hiện ngay khi backend đổi kiểu ngầm, thiếu field, hay trả giá trị enum lạ — trước khi lỗi kịp chạm production. Postman cho sẵn tv4 (đơn giản, draft-04) và Ajv qua pm.response.to.have.jsonSchema() (mạnh, thông báo lỗi chi tiết — nên ưu tiên).

Ba nguyên tắc cần nhớ: (1) luôn đánh dấu required cho những field client thực sự phụ thuộc — quên required là lỗi phổ biến nhất; (2) cân bằng độ chặt — schema quá lỏng cho an toàn giả, quá chặt (additionalProperties: false bừa bãi) gây báo động giả khiến đội tắt nó đi; (3) luôn "phá để kiểm chứng" — sửa schema cho sai và xác nhận test đỏ, để chắc chắn lưới an toàn của bạn thật sự hoạt động. Một schema vừa phải, được cả đội tin tưởng và duy trì, đáng giá hơn nhiều một schema hoàn hảo nhưng bị bỏ xó.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi