Câu hỏi thường gặp về truy vấn và kiểm chứng: JSONPath và Schema
Kết luận: Truy vấn theo đường dẫn nên dùng JSONPath hay Pointer, chọn giữa Patch và Merge Patch thế nào, Schema chặn được gì, và chọn phiên bản draft ra sao — tất cả trong một lượt.

Tóm lại: dùng JSONPath để tìm nút theo điều kiện, JSON Pointer hoặc JSON Patch để trỏ và sửa vị trí chính xác, và Merge Patch để gộp cấu hình; hãy gắn Schema mới với 2020-12 và bật allErrors để mọi lỗi đều trả về trong một lượt.
Truy vấn là "tìm nó"; kiểm chứng là "xác nhận nó đúng". Mỗi bên có chuẩn riêng, nên đừng trộn chúng với nhau.
1. Truy vấn theo đường dẫn
Nên chọn JSONPath hay JSON Pointer?
Nó tùy vào việc bạn đang "tìm theo điều kiện" hay "lấy theo vị trí".
| Khía cạnh | JSONPath | JSON Pointer |
|---|---|---|
| Chuẩn | RFC 9535 (2024) | RFC 6901 (2013) |
| Biểu thức | $.users[?(@.age > 18)].id |
/users/0/id |
| Wildcard và đệ quy | Hỗ trợ *, .. |
Không hỗ trợ |
| Lọc theo điều kiện | Hỗ trợ | Không hỗ trợ |
| Kết quả | Một tập nút (có thể nhiều nút) | Một vị trí duy nhất |
| Khả năng biểu đạt | Mạnh, nhưng mang nặng di sản khác biệt giữa các hiện thực | Yếu, nhưng hoàn toàn tất định |
| Dùng điển hình | Trích log, gỡ lỗi, khám phá dữ liệu | JSON Patch, trỏ cấu hình, đường dẫn lỗi |
Quy tắc chọn rất trực tiếp:
- Cần "lấy tất cả các nút khớp một điều kiện" → JSONPath;
- Cần "xác định một vị trí chính xác rồi thêm, xóa hay sửa nó" → JSON Pointer;
- Dùng cùng nhau: tìm nút bằng JSONPath rồi mô tả vị trí của nó bằng Pointer.
Đừng dùng JSONPath để trỏ trong hợp đồng API. JSONPath trả về một tập, và ba nhánh kết quả rỗng, một phần tử, nhiều phần tử làm giao thức phức tạp; Pointer nghĩa là "hoặc nó trỏ tới một vị trí tồn tại, hoặc không tồn tại", phù hợp với hợp đồng hơn.
$.. và $.* khác nhau thế nào?
$.. là đệ quy đi xuống và đi qua mọi cấp; $.* chỉ trả về các con trực tiếp của cấp hiện tại.
{
"a": { "id": 1, "child": { "id": 2 } },
"b": { "id": 3 }
}
$.*→ hai object{id:1, child:{...}}và{id:3};$..id→ ba giá trị:1,2,3;$.*.id→1và3, bỏ qua2lồng bên trong.
Đệ quy đi xuống tiện nhưng tốn kém: $.. đi qua toàn bộ cây, là O(n) trên một tài liệu lớn, và dễ vô tình vơ phải những trường sâu mà bạn không định lấy.
Nếu chỉ cần một cấp cụ thể, hãy tránh ...
Bộ lọc có so sánh được chuỗi không? Có khớp bằng regex không?
So sánh chuỗi thì được; regex còn tùy hiện thực.
Cú pháp chuẩn (RFC 9535):
$.items[?(@.status == "active")]
$.items[?(@.price > 10 && @.stock > 0)]
$.items[?(@.name == $.target)] // compare with another node
So sánh chuỗi dùng nháy kép (RFC 9535 cũng cho phép nháy đơn, nhưng một số hiện thực cũ chỉ chấp nhận nháy kép).
So sánh số theo ngữ nghĩa số; trộn chuỗi với số, như "10" > 9, là hành vi không xác định trong chuẩn, nên đừng dựa vào nó.
Với regex, RFC 9535 định nghĩa các hàm match() và search(), nhưng mức hỗ trợ chúng khác nhau đáng kể giữa các hiện thực.
Cách an toàn: để JSONPath lọc thô, rồi làm việc khớp regex trong mã sau khi lấy được kết quả.
Nên dùng JSON Patch hay Merge Patch?
Cả hai đều mô tả "thay đổi thứ gì đó thế nào", nhưng khả năng biểu đạt và ngữ nghĩa hoàn toàn khác nhau.
| Khía cạnh | JSON Patch (RFC 6902) | Merge Patch (RFC 7386) |
|---|---|---|
| Dạng | Mảng thao tác [{"op":"add",...}] |
Một object một phần |
| Thao tác được hỗ trợ | add / remove / replace / move / copy / test | Chỉ gộp (xóa bằng null) |
| Trỏ được phần tử mảng | Có, theo chỉ số | Không, toàn bộ mảng bị thay thế |
| Tính nguyên tử | test cho phép khóa lạc quan |
Không có |
| Kích thước tài liệu | Lớn nhưng chính xác | Nhỏ nhưng hạn chế khả năng biểu đạt |
| Phù hợp cho | Sửa chi tiết, cập nhật một phần API | Gộp cấu hình, gửi form một phần |
Tiêu chí quyết định:
- Cần đổi một trường của phần tử thứ ba trong mảng → JSON Patch;
- Cần gộp vài mục cấu hình vào cấu hình hiện có → Merge Patch;
- Cần kiểm soát đồng thời kiểu "thất bại nếu giá trị gốc không phải X" →
testcủa JSON Patch.
Lưu ý cái bẫy trong ngữ nghĩa null của Merge Patch: null nghĩa là "xóa khóa này", không phải "đặt giá trị thành null".
Đặt giá trị thành null là điều không thể làm với Merge Patch.
2. Kiểm chứng bằng Schema
JSON Schema giải quyết những vấn đề gì?
Nó biến "dữ liệu nên trông như thế nào" từ một thỏa thuận bằng lời thành một hợp đồng máy kiểm tra được. Những lỗi nó thực sự chặn được:
- Thiếu trường bắt buộc (
required); - Sai kiểu, ví dụ chuỗi ở nơi cần số (
type); - Giá trị nằm ngoài phạm vi (
minimum/maximum/minLength); - Giá trị nằm ngoài tập cho phép (
enum); - Kiểu phần tử mảng không nhất quán hoặc độ dài mảng sai (
items/minItems); - Thiếu trường trong cấu trúc lồng nhau (kiểm chứng đệ quy qua
properties); - Trường thừa (
additionalProperties: false).
Những gì nó không giải quyết được: quy tắc nghiệp vụ ("thời gian kết thúc phải muộn hơn thời gian bắt đầu"), điều kiện liên trường (một số có thể biểu diễn bằng if/then,
nhưng độ dễ đọc giảm), tính duy nhất trong cơ sở dữ liệu, và kiểm tra quyền.
Vì thế Schema là người gác cổng cho "hình dạng dữ liệu", không phải thứ thay thế logic nghiệp vụ.
Nên dùng draft-07 hay 2020-12?
Dùng 2020-12 cho dự án mới; với hệ thống hiện có, hãy kiểm tra trình kiểm chứng hỗ trợ gì.
| Phiên bản | Trạng thái | Thay đổi chính |
|---|---|---|
| draft-04 | Lỗi thời | Nhiều thư viện cũ vẫn mặc định dùng nó |
| draft-07 | Chuẩn trên thực tế | Hỗ trợ rộng nhất, hệ sinh thái phong phú nhất |
| 2019-09 | Chuyển tiếp | Giới thiệu $defs, unevaluatedProperties |
| 2020-12 | Hiện được khuyến nghị | items tách thành prefixItems + items, hỗ trợ $dynamicRef |
Hai cái bẫy khi chuyển đổi:
- Cú pháp tuple đã thay đổi: trong draft-07,
itemscó giá trị là mảng biểu diễn một tuple; 2020-12 chuyển việc đó sangprefixItems; $refvà các từ khóa anh em: trong draft-07, các từ khóa anh em bị bỏ qua khi có$ref; 2020-12 cho phép chúng cùng tồn tại.
Nếu trình kiểm chứng chỉ hỗ trợ draft-07, việc viết cú pháp 2020-12 sẽ bị bỏ qua một cách âm thầm — và trường hợp nguy hiểm nhất là "kiểm chứng qua" khi thực ra chẳng có gì được kiểm chứng.
Làm sao sinh kiểu TypeScript từ một Schema?
Hai hướng: suy ra kiểu từ Schema, hoặc sinh Schema từ kiểu.
Từ Schema sang kiểu (backend định nghĩa hợp đồng, frontend tiêu thụ):
import type { FromSchema } from 'json-schema-to-ts';
const userSchema = {
type: 'object',
properties: {
id: { type: 'string' },
age: { type: 'integer' },
},
required: ['id'],
additionalProperties: false,
} as const;
type User = FromSchema<typeof userSchema>;
// { id: string; age?: number }
Mấu chốt là as const: nếu không có nó, type: 'string' được suy ra là string thay vì literal 'string', và việc suy luận sẽ hỏng.
Từ kiểu sang Schema (frontend đã có kiểu và cần kiểm chứng lúc chạy):
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
const userSchema = z.object({ id: z.string(), age: z.number().int().optional() });
const jsonSchema = zodToJsonSchema(userSchema);
Ưu điểm của hướng này là "một định nghĩa cho cả kiểu lẫn kiểm chứng lúc chạy"; cái giá là phải đưa vào một DSL,
và Schema sinh ra có thể mang chi tiết của hiện thực (ví dụ cấu trúc $ref không đẹp mắt).
Làm sao xác định trường cụ thể khi kiểm chứng thất bại?
Hãy xem instancePath (2020-12) hoặc dataPath (draft-07) mà trình kiểm chứng trả về; đó là một JSON Pointer tới vị trí lỗi.
[
{
"instancePath": "/items/2/price",
"schemaPath": "#/properties/items/items/properties/price/type",
"keyword": "type",
"message": "must be number"
}
]
instancePath: "/items/2/price" cho bạn biết trực tiếp: trường price của phần tử thứ ba trong mảng items có kiểu sai.
Hai gợi ý thực tế:
- Trả về mọi lỗi cùng lúc thay vì ném lỗi ngay lỗi đầu tiên — frontend sửa hết trong một lượt sẽ nhanh hơn nhiều lần thử đi thử lại;
- Chuyển Pointer thành ngôn ngữ bình thường trước khi hiển thị cho người dùng cuối;
/items/2/pricehữu ích cho nhà phát triển và vô dụng với người dùng.
allErrors: true là công tắc trong hầu hết trình kiểm chứng (Ajv yêu cầu bật rõ ràng); mặc định thường là chế độ fail-fast.
Có nên bật additionalProperties: false không?
Bật cho hợp đồng nội bộ; thận trọng với API công khai.
Khi đã bật, bất kỳ trường nào không được khai báo trong properties đều không qua kiểm chứng. Lợi ích là bắt được lỗi chính tả
(userNmae) và các trường đi xuyên không mong đợi; bất lợi là việc thượng nguồn thêm một trường sẽ làm hỏng phía tiêu thụ, phá vỡ tương thích ngược.
Thỏa hiệp:
| Tình huống | Khuyến nghị |
|---|---|
| Giữa các dịch vụ nội bộ | Bật. Thay đổi trường phải qua quản lý phiên bản |
| API công khai ra bên ngoài | Tắt. Cho phép thượng nguồn thêm trường |
| Tiêu thụ dữ liệu của bên thứ ba | Tắt. Bạn không kiểm soát phía bên kia |
| Kiểm chứng cấu hình | Bật. Lỗi chính tả trong cấu hình phải thất bại ngay |
Làm sao kiểm chứng các phần tử mảng là duy nhất?
Hai ngữ nghĩa khác nhau — đừng nhầm lẫn:
{ "uniqueItems": true }
Cái này khử trùng lặp toàn bộ phần tử, yêu cầu mảng không chứa hai phần tử bằng nhau sâu.
{
"type": "array",
"items": { "type": "object", "properties": { "id": { "type": "string" } } },
"uniqueItems": true
}
Đoạn trên chỉ đảm bảo các object nguyên vẹn không trùng nhau; {"id":"a","name":"x"} và {"id":"a","name":"y"} không được coi là trùng.
Muốn khử trùng theo một trường cụ thể thì Schema không biểu diễn được; bạn phải kiểm chứng ở tầng nghiệp vụ:
const ids = items.map((item) => item.id);
if (new Set(ids).size !== ids.length) {
throw new Error('id 重复');
}
format có thực sự kiểm chứng email và ngày tháng không?
Không hẳn. Trong đặc tả, format mặc định là "chú thích" chứ không phải "khẳng định", nên trình kiểm chứng có thể thu thập nó mà không kiểm chứng.
- Ajv yêu cầu cài đặt và cấu hình gói
ajv-formatsriêng, nếu khôngformat: "email"thực chất không có tác dụng; - Một số trình kiểm chứng coi
formatlà khẳng định được bật mặc định, nên hành vi không nhất quán; - Ngay cả khi được bật, regex
emailrất lỏng —a@bvẫn qua.
Kết luận: đừng coi format là kiểm chứng mạnh. Hãy kiểm chứng nghiêm ngặt email, URL và ngày tháng trong mã nghiệp vụ của bạn;
giữ format trong Schema như tài liệu là việc đáng làm, nhưng đừng mong nó chặn được dữ liệu bẩn.
3. Bảng tra nhanh
| Câu hỏi | Kết luận |
|---|---|
| Tìm nút theo điều kiện | JSONPath |
| Trỏ chính xác, áp dụng patch | JSON Pointer |
| Sửa phần tử mảng | JSON Patch, Merge Patch không làm được |
| Gộp cấu hình | Merge Patch |
| Phiên bản Schema cho dự án mới | 2020-12, lưu ý prefixItems |
| Sinh kiểu TS từ Schema | json-schema-to-ts + as const |
| Xác định lỗi kiểm chứng | Đọc instancePath, bật allErrors |
| Khử trùng theo trường | Schema không làm được; dùng Set ở tầng nghiệp vụ |
format: email |
Hầu hết trình kiểm chứng không kiểm tra mặc định, nên đừng dựa vào |