JSON Toolkit

Những điểm then chốt khi thiết kế hợp đồng dữ liệu API JSON

Kết luận: Sáu thứ rất khó thay đổi sau khi đã phát hành: vỏ bọc phản hồi, định dạng lỗi, hình dạng phân trang, cách biểu diễn ID và thời gian, ngữ nghĩa của null so với vắng mặt, và tiến hoá phiên bản.

Trung cấpĐọc khoảng 13 phútXuất bản 2026-10-30Cập nhật 2026-10-30
Ảnh bìa Những điểm then chốt khi thiết kế hợp đồng dữ liệu API JSON

Tóm lại: hãy chốt vỏ bọc phản hồi, định dạng lỗi, cách biểu diễn ID và thời gian, cùng ngữ nghĩa null so với vắng mặt ngay trong hợp đồng trước khi tranh luận chi tiết về phân trang và đặt tên; luôn bọc cấp cao nhất trong một vỏ data để sau này có thể thêm phân trang và ID truy vết mà không làm hỏng người gọi hiện có.

Thiết kế API không có đáp án chuẩn, nhưng có những quyết định "đã chọn là khó rút lại", tốt nhất nên nghĩ kỹ trước khi viết dòng mã đầu tiên.

1. Có nên bọc phản hồi hay không

Ba kiểu:

// Style A: bare response
{ "id": 1, "name": "张三" }

// Style B: data envelope
{ "data": { "id": 1, "name": "张三" } }

// Style C: data + metadata
{ "data": { "id": 1, "name": "张三" }, "meta": { "requestId": "a1b2" } }
Kiểu Ưu điểm Nhược điểm
Phản hồi trần Đơn giản nhất, frontend bớt được một lớp Không gắn được metadata; mảng ở cấp cao nhất mang rủi ro JSON hijacking và khó tiến hoá
Vỏ data Dễ thêm meta / errors / links Thêm một tầng lồng, frontend phải viết res.data.data
Data + metadata Khả năng quan sát tốt nhất Cần thoả thuận nghiêm ngặt về trường nào thuộc meta

Khuyến nghị: dùng thống nhất vỏ data cho cả danh sách lẫn chi tiết, và đừng bao giờ để cấp cao nhất là mảng. Như vậy, việc thêm phân trang hay ID truy vết về sau sẽ không phá vỡ hợp đồng.

2. Định dạng lỗi: sao chép RFC 9457

Tự phát minh cấu trúc lỗi là một nguồn chi phí phối hợp rất lớn. RFC 9457 (thay thế RFC 7807) định nghĩa application/problem+json:

{
  "type": "https://bbdrug.com/problems/insufficient-balance",
  "title": "余额不足",
  "status": 422,
  "detail": "账户余额 12.00 元,本次扣款需要 30.00 元",
  "instance": "/accounts/A1001/payments/p-889",
  "balance": "12.00",
  "required": "30.00"
}
Trường Ý nghĩa Bắt buộc
type URI loại vấn đề ổn định Khuyến nghị
title Tóm tắt ngắn cho con người đọc Khuyến nghị
status Mã trạng thái HTTP Khuyến nghị
detail Giải thích cụ thể lần này chuyện gì đã xảy ra Tuỳ chọn
instance Instance tài nguyên gặp vấn đề Tuỳ chọn
Trường mở rộng Trường nghiệp vụ tuỳ ý Tuỳ chọn

Đặc tả cho phép dùng trường mở rộng (như balance / required ở trên), và frontend có thể dùng chúng để tạo thông báo chính xác thay vì phân tích văn bản detail.

3. Phân trang: cursor tốt hơn offset

// Offset pagination
{ "data": [], "page": { "offset": 40, "limit": 20, "total": 137 } }

// Cursor pagination
{ "data": [], "page": { "nextCursor": "eyJpZCI6MTAwfQ", "hasMore": true } }
Khía cạnh Phân trang offset Phân trang cursor
Độ khó triển khai Thấp (LIMIT/OFFSET) Trung bình (cần một khoá sắp xếp ổn định)
Hiệu năng ở trang sâu Kém (offset càng lớn càng chậm) Tốt
Khi dữ liệu thay đổi Trùng lặp hoặc thiếu mục Ổn định
Nhảy đến một trang Có thể Không thể
Cần tổng số Cần thêm một truy vấn đếm Không cần

Khuyến nghị: dùng phân trang offset cho bảng quản trị (cần nhảy trang và tổng số), và phân trang cursor cho luồng vô hạn hướng người dùng.

4. ID và thời gian

4.1 Luôn dùng chuỗi cho ID

Lý do đã được trình bày trong bài viết về kiểu dữ liệu: ở phía JavaScript, giá trị vượt quá 2^53 sẽ âm thầm mất độ chính xác. Kể cả khi ID hiện tại là số nguyên tự tăng nhỏ, dùng chuỗi vẫn để lại dư địa chuyển sang snowflake ID về sau.

{ "id": "1000000000000000001" }

4.2 Dùng chuỗi RFC 3339 cho thời gian

{
  "createdAt": "2026-10-30T10:41:45+08:00",
  "updatedAt": "2026-10-30T02:41:45Z"
}
  • Đừng dùng timestamp số mili-giây: khó đọc, và đơn vị (giây/mili-giây) rất dễ bị nhầm;
  • Chỉ định rõ múi giờ: hoặc bằng Z hoặc bằng offset; đừng bao giờ dùng "giờ địa phương không có múi giờ";
  • Đặt tên trường thống nhất là xxxAt để nhìn là biết ngay đó là một thời điểm.

5. null và vắng mặt

Bạn phải chọn một cách và ghi nó vào tài liệu:

Cách tiếp cận Biểu đạt "không có giá trị" Biểu đạt "không sửa"
Cách A Giá trị trường là null Bỏ trường trong request
Cách B Trường không tồn tại Bỏ trường trong request

Vấn đề của cách A là "bỏ qua" và "gửi null" mang nghĩa khác nhau trong một PATCH, rất dễ bị hiểu sai; vấn đề của cách B là JSON.stringify ở frontend sẽ bỏ các trường có giá trị undefined, tình cờ khớp đúng ngữ nghĩa.

Khuyến nghị thực tế:

  • Dùng cách A cho body phản hồi (null rõ ràng nghĩa là không có giá trị, và frontend không cần phân biệt undefined);
  • Dùng cách B cho body request (đặc biệt là PATCH), kết hợp Merge Patch hoặc JSON Patch để ngữ nghĩa rõ ràng.

6. Đặt tên và cấu trúc trường

Quyết định Lựa chọn phổ biến Lưu ý
Kiểu đặt tên camelCase (hệ sinh thái JS) / snake_case (Python, cơ sở dữ liệu) Thống nhất trong toàn dự án, đừng trộn lẫn
Giá trị enum Chữ thường có gạch dưới, "pending_review" Đừng dùng số; khó đọc
Trường boolean isActive / enabled Đừng dùng status: 0/1
Trường mảng Danh từ số nhiều, items Trả về [] cho mảng rỗng, đừng bao giờ trả null
Độ sâu lồng Không quá 3 cấp Sâu hơn thì tách ra hoặc cung cấp tham số expand

Việc trả [] thay vì null cho mảng rỗng rất quan trọng: data.items.map(...) ở frontend sẽ không sụp vì một null.

7. Chiến lược tiến hoá

Thay đổi Phá vỡ tương thích Cách xử lý
Thêm một trường tuỳ chọn Không Phát hành trực tiếp
Thêm một trường bắt buộc Có Thêm giá trị mặc định, hoặc phát hành theo phiên bản
Xoá một trường Có Đánh dấu deprecated trước, quan sát người gọi, rồi mới xoá
Đổi kiểu của một trường Có Thêm tên trường mới và ghi kép trong giai đoạn chuyển tiếp
Đổi tập giá trị enum Tuỳ Client phải có nhánh dự phòng cho giá trị chưa biết
Đổi ngữ nghĩa trường Có Coi như một trường mới

Ba quy tắc kỷ luật:

  1. Client phải chịu được trường chưa biết (đừng tự giam mình bằng additionalProperties: false);
  2. Client phải có nhánh dự phòng cho giá trị enum chưa biết;
  3. Giữ trường deprecated ít nhất một phiên bản major, và đánh dấu bằng Deprecation trong header phản hồi hoặc tài liệu.

8. Một ví dụ đầy đủ

{
  "data": [
    {
      "id": "1001",
      "name": "JSON 权威指南",
      "price": "89.50",
      "tags": ["json", "spec"],
      "publishedAt": "2026-09-01T00:00:00+08:00",
      "author": null
    }
  ],
  "page": { "nextCursor": null, "hasMore": false },
  "meta": { "requestId": "req-7f3a", "tookMs": 12 }
}

Phản hồi này phản ánh toàn bộ các quy ước ở trên: vỏ data, ID dạng chuỗi, số tiền dạng chuỗi, thời gian có kèm múi giờ, null nghĩa là thiếu, mảng rỗng, phân trang cursor, và meta cho thông tin quan sát.

9. Danh sách kiểm tra

  • [ ] Cấp cao nhất không phải mảng; dùng thống nhất vỏ data;
  • [ ] Phản hồi lỗi theo RFC 9457 và có type ổn định;
  • [ ] Hình dạng phân trang khớp với tình huống nghiệp vụ;
  • [ ] ID là chuỗi, và thời gian dùng RFC 3339;
  • [ ] Ngữ nghĩa của null và vắng mặt được ghi vào tài liệu;
  • [ ] Kiểu đặt tên thống nhất trong toàn dự án, và enum dùng chuỗi chữ thường;
  • [ ] Client có dự phòng cho trường chưa biết và giá trị enum chưa biết;
  • [ ] Thay đổi phá vỡ tương thích đi qua phiên bản hoá hoặc giai đoạn ghi kép.

FAQ

Nên thiết kế API JSON có thể tiến hoá như thế nào?

Bọc thống nhất danh sách và chi tiết trong một vỏ data và đừng bao giờ để cấp cao nhất là mảng, để có thể thêm phân trang và ID truy vết mà không phá vỡ hợp đồng. Dùng ID dạng chuỗi và timestamp RFC 3339, và ghi ngữ nghĩa null so với vắng mặt vào tài liệu. Giữ trường mới là tuỳ chọn và độc lập về ngữ nghĩa, đồng thời trang bị cho client nhánh dự phòng cho trường và giá trị enum chưa biết.

Vì sao không nên tự phát minh định dạng lỗi?

Tự phát minh cấu trúc lỗi là một nguồn chi phí phối hợp rất lớn. Hãy sao chép application/problem+json của RFC 9457, nhận diện vấn đề bằng một type ổn định, và cho phép các trường mở rộng như balance và required, để frontend có thể dựng thông báo chính xác mà không phải phân tích văn bản detail.

Có phải mọi endpoint đều nên dùng phân trang cursor?

Không. Bảng quản trị cần nhảy trang và tổng số, nên phân trang offset phù hợp hơn; luồng vô hạn hướng người dùng nên dùng phân trang cursor, vốn ổn định ở trang sâu và tránh trùng lặp hay khoảng trống khi dữ liệu thay đổi. Hãy chọn dựa trên việc tình huống có cần nhảy trang và tổng số hay không.

Tài liệu tham khảo

Công cụ phù hợp

Bài viết liên quan

Sáu kiểu, độ chính xác số và cạm bẫy mã hóa

JSON chỉ có sáu kiểu, nhưng mỗi kiểu đều có thể âm thầm định hình lại dữ liệu của bạn. Bài viết này gói gọn cạm bẫy của số, chuỗi và null trong một lượt.

Người mớiĐọc khoảng 12 phútCập nhật 2026-10-30

JSON Pointer, Patch và Merge Patch

Ba đặc tả ngắn tạo thành một giao thức thay đổi: Pointer định vị, Patch sửa chính xác, Merge Patch hợp nhất từng phần. Chọn sai sẽ làm đau mảng và điều kiện đồng thời.

Trung cấpĐọc khoảng 12 phútCập nhật 2026-10-30

Quản lý JSON cho cấu hình, fixture và dữ liệu cục bộ

Cấu hình phải được phân tầng và kiểm chứng; fixture phải tất định, dễ đọc và có thể so sánh diff. Nếu hai loại tệp này bị quản lý kém, chi phí xử lý sự cố sẽ cao trong thời gian dài.

Người mớiĐọc khoảng 10 phútCập nhật 2026-10-30