JSON Toolkit

Cùng một JSON, bốn công cụ, bốn kết quả: chọn trình định dạng thế nào

Kết luận: Định dạng không phải là không mất dữ liệu. Cùng một JSON có số bắt đầu bằng 0, BOM hoặc số nguyên 20 chữ số lại cho kết quả khác nhau ở Python, Node, jq và json.tool: có nơi báo lỗi, có nơi lặng lẽ đổi giá trị. Mười mẫu tự chạy trên bốn đường xử lý cho ra bảng chọn công cụ và đoạn script kiểm tra chặt chẽ.

IntermediateĐọc khoảng 6 phútXuất bản 2026-11-03Cập nhật 2026-11-03
Bốn đường phân tích cho kết quả khác nhau với cùng một JSON

Kết luận: khác biệt chí mạng giữa các công cụ định dạng JSON không phải là thụt lề mấy dấu cách, mà là cùng một đầu vào, có công cụ báo lỗi, có công cụ âm thầm đổi dữ liệu. Trước khi chọn, hãy cho một mẫu có giá trị biên chạy qua công cụ — nó nói rõ hơn cả danh sách tính năng.

Người dùng tưởng định dạng chỉ là «sắp xếp lại văn bản». Thực tế mỗi đường đều dùng parser của riêng mình để diễn giải lại văn bản đó. Parser càng dễ dãi, lẽ ra phải ném lỗi thì lại thành sửa dữ liệu im lặng, trong khi trang vẫn hiện một dòng xanh «Định dạng thành công» đầy an tâm.

1. Hiện tượng: một đầu vào, bốn đường, bốn kết quả

Môi trường thử nghiệm: Python 3.11.1, Node v22.13.1, jq 1.7. Mười chuỗi JSON ngắn dựng tay được đưa qua stdin, không ghi đĩa, không đổi cấu hình. Bốn đường: json.loads, JSON.parse, jq -c ., python -m json.tool. ERR nghĩa là ném ngoại lệ và dừng, SILENT nghĩa là không báo lỗi nhưng đầu ra đã khác đầu vào.

Mẫu python json.loads node JSON.parse jq -c . python -m json.tool
S1 {"a":1,"b":[1,2]} OK OK OK OK
S2 {"a":1,} ERR ERR ERR ERR
S3 {'a':1} ERR ERR ERR ERR
S4 {"a":01} ERR char 6 ERR position 6 SILENT {"a":1} ERR
S5 {"a":1 /*x*/} ERR char 7 ERR ERR ERR
S6 \ufeff{"a":1} ERR utf-8-sig ERR position 0 SILENT {"a":1} ERR
S7 {"id":9007199254740993} OK exact SILENT 9007199254740992 OK exact OK
S8 {"a":1,"a":2} SILENT {"a":2} SILENT {"a":2} SILENT {"a":2} OK
S9 {"a":1 ERR char 6 ERR ERR EOF ERR
S10 <html>502</html> ERR char 0 ERR Unexpected token '<' ERR ERR

Bốn trong mười mẫu cho «không lỗi nhưng dữ liệu đổi»: S4, S6, S7, S8. Nghĩa là chỉ tin vào đèn xanh «Định dạng thành công», bạn có bốn mươi phần trăm khả năng đang cầm dữ liệu không còn nguyên bản. Điều dính bẫy là cả bốn đều không nằm trong danh sách lỗi cú pháp — file mở được, copy được, export được, lỗi nằm ở nội dung, không phải cú pháp.

2. Nguyên nhân: tầng cú pháp và tầng ngữ nghĩa là hai chuyện khác nhau

RFC 8259 nói thẳng: mục 1.2 khuyến nghị tránh một số thực hành mà ECMA-404 cho phép để tối đa khả năng tương tác; mục 4 yêu cầu tên đối tượng SHOULD là duy nhất nhưng không quy định xử lý khóa trùng; mục 6 bàn về số chỉ đưa khuyến nghị tương tác. ECMA-404 đi xa hơn, viết rõ là «bất khả tri với ngữ nghĩa của số» — chỉ quy định cách biểu diễn chuỗi số, không quy định lưu trữ trong ngôn ngữ. Nói cách khác, đặc tả để lại cho người hiện thực một vùng biên rất rộng.

Vì thế «định dạng» tách thành hai tầng: tầng cú pháp quyết định văn bản có hợp lệ, tầng ngữ nghĩa quyết định văn bản đó biến thành giá trị gì. Cái trước có đáp án chuẩn, cái sau thì không. jq đọc 01 thành 1, bỏ qua BOM đầu dòng là lựa chọn ở tầng ngữ nghĩa; Node ép số nguyên 20 chữ số vào double IEEE 754 là lựa chọn của runtime; bốn đường cùng giữ giá trị cuối cho {"a":1,"a":2} là thông lệ chung của các parser lớn. Không cái nào vi phạm «parse được», nhưng tất cả đều đổi dữ liệu.

Cách sửa lỗi phân tích JSON xử lý tầng cú pháp, dịch nguyên văn báo lỗi thành tọa độ. Bài này xử lý bước ngay trước đó — nếu công cụ chẳng báo lỗi gì, bạn còn chẳng có tọa độ mà dùng.

3. Bốn lần đổi dữ liệu im lặng, nhìn từng cái

Tách riêng bốn mẫu im lặng, các thay đổi nhỏ đến mức lướt qua bằng mắt thường không thấy:

{"a": 01}                          // leading zero: jq emits {"a":1}
\ufeff{"a": 1}                     // BOM: jq skips the first three bytes
{"id": 9007199254740993}           // 20-digit int: JSON.parse gives ...992
{"a": 1, "a": 2}                   // duplicate key: all four keep {"a":2}

Số zero đầu và BOM thuộc dạng «đầu vào vốn đã không hợp lệ, nhưng công cụ đã dễ dãi». 01 là bất hợp pháp trong JSON chuẩn (RFC 8259 mục 6), BOM cũng không nên đứng đầu văn bản JSON; Python và Node đều báo lỗi rõ ràng, jq lại chọn tiếp tục. Kiểu dễ dãi này rất tiện khi làm sạch log, nhưng là thảm họa khi kiểm tra hợp đồng: upstream tưởng mình gửi dữ liệu sai cần sửa, downstream đã cầm một kết quả «trông có vẻ bình thường».

Số nguyên lớn thuộc dạng «đầu vào hoàn toàn hợp lệ, runtime không chứa nổi». 9007199254740993 là số JSON hợp lệ nhưng vượt 2^53-1, JSON.parse chỉ biểu diễn được 9007199254740992. int của Python và jq đều giữ nguyên giá trị. Nếu trong JSON này là mã đơn hàng hay Snowflake ID, đổi một chữ số cuối nghĩa là đối chiếu sổ sách không khớp.

Khóa trùng thuộc dạng «đặc tả không quy định, hiện thực tự chọn». Cả bốn đường đều im lặng lấy giá trị sau, không một cái cảnh báo. RFC 8259 chỉ nói SHOULD nên duy nhất, nên đây không phải bug, nhưng nó là lớp lỗi khó tìm nhất khi ghép cấu hình hay merge patch.

4. Khả năng định vị lỗi: ai cho dòng cột, ai chỉ cho một câu

Xem tiếp sáu mẫu lỗi, lượng thông tin bốn đường đưa ra chênh nhau rất xa. JSONDecodeError của Python cho đồng thời msg, lineno, colno, pos; Node v22 báo kèm position N (line 1 column M) và đoạn văn bản tại chỗ lỗi; python -m json.tool dùng lại thông tin của Python; jq chỉ cho một câu parse error: ... at line N, không có offset ký tự.

Với JSON nén một dòng, số dòng cột gần như vô dụng — mọi thứ đều ở dòng 1. Thứ cần là offset ký tự. Đoạn script dưới chuẩn hóa việc kiểm tra thành «lỗi + offset + ngữ cảnh»:

import json, sys

def strict_check(text: str) -> None:
    try:
        json.loads(text)
        print("OK")
    except json.JSONDecodeError as e:
        print(f"ERR {e.msg} | char {e.pos} | line {e.lineno} col {e.colno}")
        lo, hi = max(0, e.pos - 30), min(len(text), e.pos + 30)
        print("...", text[lo:hi].replace("\n", "\\n"), "...")
        sys.exit(1)

strict_check(open(sys.argv[1], encoding="utf-8").read())

Nó chặt hơn jq . (từ chối zero đầu và BOM) và cho e.pos. Cách đổi line 1 column 23611 thành vị trí byte, và vì sao offset ký tự với offset byte trong tài liệu CJK lệch ba lần, đã được Gỡ lỗi JSON đã nén nói kỹ, không lặp ở đây.

5. Sửa thế nào: tách định dạng thành «kiểm tra chặt» + «làm đẹp»

Kết luận rất đơn giản: đừng để một nút «Định dạng» gánh cả kiểm tra lẫn làm đẹp. Dùng parser chặt phán xét tính hợp lệ trước, rồi mới quyết định dùng công cụ nào để trình bày. Một dòng lệnh tách được bước đầu:

python3 -m json.tool input.json > /dev/null && echo "syntax OK" || echo "invalid syntax"
python3 -c "import json,sys; d=json.load(open(sys.argv[1],encoding='utf-8')); print(type(d).__name__, len(d))" input.json

Bước một chỉ trả lời «có parse được không», bước hai mới kiểm tra «parse ra có đúng cấu trúc mình kỳ vọng không». Nếu dữ liệu có số nguyên lớn, đọc bằng Python hoặc jq, đừng đi JSON.parse; nếu nguồn có thể dính BOM, hãy chỉ rõ encoding='utf-8-sig' khi đọc thay vì trông chờ công cụ tự bỏ qua; nếu khóa có thể trùng, phải quét riêng một lượt trước khi parse, vì bốn đường đều không nhắc bạn. Viết mấy điều này vào CI chính là pipeline mà Trình soạn thảo, dòng lệnh và chuỗi công cụ tự động hoá mô tả, chỉ thêm một tầng khẳng định trung thực dữ liệu.

6. Chọn thế nào: bảng so sánh bốn đường

Đường Điểm mạnh Rủi ro Hợp cho
python json.loads / json.tool Chặt, cho dòng cột và offset ký tự, số nguyên không mất độ chính xác Từ chối zero đầu và BOM, phải xử lý encoding rõ ràng Kiểm tra hợp đồng, CI, truy vết log
node JSON.parse Gốc trong trình duyệt, lỗi kèm đoạn văn bản gốc Số nguyên lớn bị cụt im lặng, BOM gây lỗi Debug frontend, công cụ trong trình duyệt
jq -c . Thân thiện với pipe, ra kết quả nhanh nhất Zero đầu và BOM lọt im lặng, không có offset ký tự Pipeline vận hành, lấy field nhanh
Công cụ trình duyệt /parse, /editor Xem dạng cây, sắp xếp khóa, copy đường dẫn, không cần cài gì Hành vi phụ thuộc parser của trang, phải thử một lần Xem tay, dò lỗi tạm thời

Chìa khóa không phải «cái nào nhiều tính năng», mà là «nó có nói dối về dữ liệu của mình không». Với dữ liệu chứa mã đơn, số tiền, ID thiết bị, ưu tiên Python hoặc công cụ tuyên bố rõ giữ nguyên dữ liệu; chỉ xem cấu trúc, không lưu kho thì trình duyệt là tiện nhất. /parse của site này chạy kiểm tra cú pháp và định dạng ngay trong trình duyệt, hợp cho «nhìn cho rõ đã», nhưng khi dính số nguyên lớn vẫn nên đối chiếu lại bằng script trên.

7. Riêng tư chỉ là một trong các ranh giới

«Dữ liệu có bị upload không» là câu hỏi thường gặp nhất khi chọn công cụ online, nhưng nó độc lập với «dữ liệu có bị đổi không»: công cụ chạy local, dùng offline được vẫn có thể cụt số nguyên 20 chữ số; ngược lại, một công cụ online báo lỗi rất chặt vẫn có thể POST nguyên văn lên server. Hai chuyện phải kiểm tra riêng, không thay thế được nhau. Bài FAQ về riêng tư đưa ba động tác kiểm tra trong hai phút; bài này bổ sung nửa còn lại — dùng danh sách mẫu dưới đây để xác nhận công cụ không đổi giá trị im lặng.

8. Một danh sách tự kiểm tra có thể copy

Lưu S1–S10 thành cases.txt, mỗi dòng một mẫu, dán vào bất kỳ công cụ ứng viên nào và đối chiếu ba việc: mẫu lỗi có thật sự báo lỗi không, lỗi có nêu vị trí cụ thể không, đầu ra của mẫu không lỗi có từng ký tự khớp đầu vào không. Đặc biệt soi bốn dòng S4, S6, S7, S8 — chỉ cần một dòng SILENT là đường đó không thể dùng đơn lẻ để kiểm tra dữ liệu.

Thêm ba ràng buộc kỹ thuật: đưa kiểm tra chặt vào pre-commit hook, đừng đợi lên production mới phát hiện số nguyên lệch; ghi file UTF-8 không BOM thống nhất đa ngôn ngữ; với JSON từ bên thứ ba giữ lại nguyên body phản hồi, kết quả đã định dạng chỉ để đọc, không dùng để ghi ngược lại.

Câu hỏi thường gặp

Định dạng thành công có nghĩa là JSON ổn chưa?

Không. S4, S6, S7, S8 đều «qua» định dạng, nhưng zero đầu, BOM, số nguyên 20 chữ số và khóa trùng đều bị ghi đè hoặc nuốt mất. Định dạng chỉ chứng minh văn bản được một parser nào đó chấp nhận, không chứng minh giá trị sau parse bằng giá trị bạn gửi; field quan trọng phải khẳng định từng giá trị riêng.

Vì sao jq chấp nhận zero đầu và BOM mà Python báo lỗi?

jq có bộ phân tích từ vựng riêng, dễ dãi với số kiểu 01 và cũng bỏ qua BOM đầu dòng; Python và Node bám RFC 8259 chặt chẽ. Đây không phải bug phiên bản, mà là lựa chọn giữa parser dễ dãi và parser chặt. Làm sạch dữ liệu thì cái trước đỡ việc, kiểm tra hợp đồng thì bắt buộc cái sau.

Tránh để số nguyên lớn thành 9007199254740992 thế nào?

Number của JavaScript là double IEEE 754 với ngưỡng an toàn 2^53-1. Đọc ID 20 chữ số bằng JSON.parse thế nào cũng cụt. Hãy lưu các ID này dạng chuỗi, hoặc dùng int của Python, jq, hay thư viện parse hỗ trợ BigInt; nếu frontend thật sự cần số, thay field đó thành chuỗi trước khi parse.

Nên dùng công cụ online hay dòng lệnh local?

Chia theo độ nhạy và mục đích: chỉ xem cấu trúc, tìm field thì trình duyệt nhanh nhất; vào pipeline, cần khẳng định trung thực dữ liệu thì python -m json.tool cộng script kiểm tra chặt. Cả hai đều nên chạy S1–S10 trước, đối chiếu lỗi và đầu ra với kỳ vọng, rồi mới quyết định đưa lớp dữ liệu nào vào.