JSON Toolkit

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.

Trung cấpĐọc khoảng 12 phútXuất bản 2026-10-30Cập nhật 2026-10-30
Ảnh bìa câu hỏi thường gặp về truy vấn và kiểm chứng

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 → 1 và 3, bỏ qua 2 lồ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" → test củ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:

  1. Cú pháp tuple đã thay đổi: trong draft-07, items có giá trị là mảng biểu diễn một tuple; 2020-12 chuyển việc đó sang prefixItems;
  2. $ref và 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ế:

  1. 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;
  2. 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/price hữ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-formats riêng, nếu không format: "email" thực chất không có tác dụng;
  • Một số trình kiểm chứng coi format là 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 email rất lỏng — a@b vẫ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