Cách Định Dạng JSON Trong JavaScript — 5 Phương Pháp
JSON có ở khắp nơi: phản hồi API, tệp cấu hình, xuất cơ sở dữ liệu, pipeline log. Nhưng đầu ra JSON thô thường là một khối gọn — không ngắt dòng, không thụt dòng, các khóa theo bất kỳ thứ tự nào mà bộ tuần tự hóa đưa ra. Điều đó làm cho việc gỡ lỗi đau đớn và cộng tác khó khăn hơn cần thiết.
JavaScript cung cấp cho bạn nhiều cách để định dạng JSON, từ JSON.stringify tích hợp đến
các thư viện streaming cho các tệp quá lớn để tải vào bộ nhớ. Hướng dẫn này đi qua năm phương pháp
thực tế, bao quát các trường hợp phổ biến nhất và các trường hợp biên khiến các nhà phát triển bất ngờ.
Phương Pháp 1: JSON.stringify Với Thụt Dòng
Cách tiếp cận đơn giản nhất đã được tích hợp vào ngôn ngữ. JSON.stringify chấp nhận ba
đối số: giá trị để tuần tự hóa, một replacer (thêm về điều đó trong Phương Pháp 2), và một tham số space
kiểm soát thụt dòng.
const data = {
name: "Alice",
role: "engineer",
skills: ["JavaScript", "TypeScript", "Node.js"],
active: true,
};
// Thụt dòng 2 khoảng trắng (mặc định phổ biến)
console.log(JSON.stringify(data, null, 2));
// Thụt dòng 4 khoảng trắng
console.log(JSON.stringify(data, null, 4));
// Thụt dòng tab
console.log(JSON.stringify(data, null, "\t"));
Với null, 2, đầu ra trông như thế này:
{
"name": "Alice",
"role": "engineer",
"skills": [
"JavaScript",
"TypeScript",
"Node.js"
],
"active": true
}
Hai khoảng trắng là quy ước phổ biến nhất trong các dự án JavaScript — nó khớp với mặc định trong ESLint,
Prettier và hầu hết các hướng dẫn phong cách. Bốn khoảng trắng phổ biến trong các công cụ liền với Python. Khi
space bị bỏ qua hoặc đặt thành 0, đầu ra là một chuỗi gọn một dòng —
lý tưởng cho truyền tải qua mạng.
Bạn cũng có thể định dạng JSON ngay lập tức trong trình duyệt bằng công cụ JSON Formatter — dán JSON thô, nhận đầu ra được in đẹp và sao chép kết quả mà không viết bất kỳ code nào.
Phương Pháp 2: Hàm Replacer Tùy Chỉnh
Đối số thứ hai của JSON.stringify — replacer — là một hàm hoặc mảng lọc và biến đổi các giá trị
trước khi tuần tự hóa. Đây là nơi bạn có kiểm soát thực sự về những gì kết thúc trong đầu ra.
Dạng mảng là cách tiếp cận đơn giản nhất cho danh sách trắng khóa:
const user = {
id: 42,
name: "Bob",
password: "s3cr3t",
email: "bob@example.com",
createdAt: new Date(),
};
// Replacer mảng: chỉ bao gồm các khóa được liệt kê
const safeJson = JSON.stringify(user, ["id", "name", "email"], 2);
console.log(safeJson);
// {
// "id": 42,
// "name": "Bob",
// "email": "bob@example.com"
// }
Dạng hàm cho bạn kiểm soát từng giá trị. Trả về giá trị để bao gồm nó, trả về
undefined để loại bỏ, hoặc trả về một giá trị đã biến đổi:
function replacer(key, value) {
// Loại bỏ các khóa bắt đầu bằng gạch dưới (quy ước riêng tư)
if (key.startsWith("_")) return undefined;
// Ẩn các trường nhạy cảm
if (key === "password" || key === "token") return "[REDACTED]";
// Chuyển các đối tượng Date thành chuỗi ISO một cách rõ ràng
if (value instanceof Date) return value.toISOString();
return value;
}
const payload = {
id: 1,
name: "Carol",
password: "hunter2",
_internalFlag: true,
lastLogin: new Date("2026-05-01"),
};
console.log(JSON.stringify(payload, replacer, 2));
// {
// "id": 1,
// "name": "Carol",
// "password": "[REDACTED]",
// "lastLogin": "2026-05-01T00:00:00.000Z"
// } Replacer chạy đệ quy trên mỗi object lồng và mảng. Mẫu này hữu ích cho các pipeline ghi log nơi bạn muốn loại bỏ thông tin xác thực trước khi ghi vào đĩa hoặc gửi đến một dịch vụ quan sát.
Phương Pháp 3: In Đẹp Với Khóa Đã Sắp Xếp
Các object JavaScript không đảm bảo thứ tự khóa (mặc dù V8 và hầu hết các engine bảo toàn thứ tự chèn cho các khóa chuỗi). Khi bạn cần đầu ra quyết định — cho diff, cache hoặc biểu diễn chuẩn — sắp xếp các khóa theo thứ tự bảng chữ cái là động thái đúng.
function sortedStringify(value, indent = 2) {
return JSON.stringify(value, sortReplacer, indent);
}
function sortReplacer(key, value) {
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
return Object.keys(value)
.sort()
.reduce((sorted, k) => {
sorted[k] = value[k];
return sorted;
}, {});
}
return value;
}
const config = {
version: "1.0",
author: "Dave",
dependencies: { typescript: "^5.4", eslint: "^9.0", astro: "^5.0" },
name: "my-project",
};
console.log(sortedStringify(config));
// {
// "author": "Dave",
// "dependencies": { "astro": "^5.0", "eslint": "^9.0", "typescript": "^5.4" },
// "name": "my-project",
// "version": "1.0"
// }
Các khóa đã sắp xếp có nghĩa là các diff git chỉ hiển thị các dòng thực sự thay đổi, thay vì các sắp xếp lại tùy ý
từ các bộ tuần tự hóa khác nhau. Điều này đặc biệt hữu ích cho package.json
và các tệp cấu hình tương tự được check vào kiểm soát phiên bản.
Để chuyển JSON sang các định dạng khác, công cụ JSON to YAML và công cụ JSON to CSV cũng xử lý sắp xếp khóa trong đầu ra của chúng.
Phương Pháp 4: Định Dạng Từ Một Chuỗi (Parse + Stringify)
JSON trong thế giới thực thường đến dưới dạng chuỗi — từ phản hồi fetch, đọc tệp, dán clipboard, hoặc cột TEXT của cơ sở dữ liệu. Bạn cần phân tích nó trước, sau đó định dạng lại. Phần quan trọng là xử lý lỗi đúng cách: JSON không hợp lệ sẽ throw, và bạn muốn bắt nó một cách nhẹ nhàng.
function formatJsonString(rawString, indent = 2) {
try {
const parsed = JSON.parse(rawString);
return { ok: true, result: JSON.stringify(parsed, null, indent) };
} catch (err) {
return { ok: false, error: err.message };
}
}
const raw = '{"name":"Eve","scores":[100,95,88],"active":true}';
const { ok, result, error } = formatJsonString(raw);
if (ok) {
console.log(result);
// {
// "name": "Eve",
// "scores": [100, 95, 88],
// "active": true
// }
} else {
console.error("Phân tích thất bại:", error);
}
// Đầu vào không hợp lệ
const bad = '{"name": "Eve", "broken":}';
const r2 = formatJsonString(bad);
// { ok: false, error: "Unexpected token '}'" } Bọc các lỗi phân tích trong một đối tượng trả về có cấu trúc làm cho hàm này an toàn để dùng trong các thành phần UI và script build mà không bao quanh mỗi call site trong try/catch. Tài liệu MDN cho JSON.stringify bao quát đặc tả tham số đầy đủ, và RFC 8259 định nghĩa JSON hợp lệ trông như thế nào ở cấp giao thức.
Phương Pháp 5: Streaming Cho Các Tệp Lớn
Các Phương Pháp 1–4 đều tải toàn bộ cấu trúc JSON vào bộ nhớ trước khi định dạng. Đối với các tệp trong phạm vi hàng trăm megabyte hoặc nhiều gigabyte, điều này chặn event loop của Node.js và có thể crash tiến trình hoàn toàn.
Cách tiếp cận streaming đọc tệp theo các chunk và ghi đầu ra đã định dạng tăng dần. Đối với NDJSON
(một object JSON mỗi dòng, phổ biến trong các tệp log và xuất cơ sở dữ liệu), một cách tiếp cận dựa trên
readline hoạt động mà không có dependency bổ sung:
import { createReadStream, createWriteStream } from "node:fs";
import { createInterface } from "node:readline";
async function formatNdjsonFile(inputPath, outputPath) {
const rl = createInterface({
input: createReadStream(inputPath),
crlfDelay: Infinity,
});
const output = createWriteStream(outputPath);
for await (const line of rl) {
if (!line.trim()) continue;
try {
const obj = JSON.parse(line);
output.write(JSON.stringify(obj, null, 2) + "\n---\n");
} catch (e) {
output.write("[Dòng JSON không hợp lệ: " + e.message + "]\n---\n");
}
}
output.end();
} NDJSON là định dạng streaming đơn giản nhất: mỗi dòng là một object JSON hợp lệ, hoàn chỉnh. Nhiều công cụ xuất hỗ trợ nó chính vì nó dễ dàng được stream. Nếu bạn kiểm soát định dạng của các xuất dữ liệu lớn, ưu tiên NDJSON hơn một mảng JSON khổng lồ duy nhất.
Các Trường Hợp Biên Cần Lưu Ý
Đây là các kịch bản nơi định dạng JSON tiêu chuẩn hoặc thất bại âm thầm hoặc throw bất ngờ.
Tham Chiếu Vòng Tròn
JSON.stringify throw một TypeError nếu một object tham chiếu chính nó trực tiếp
hoặc gián tiếp. Khắc phục bằng một replacer theo dõi các object đã ghé thăm dùng WeakSet:
function safeStringify(obj, indent = 2) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === "object" && value !== null) {
if (seen.has(value)) return "[Circular]";
seen.add(value);
}
return value;
}, indent);
}
const a = { name: "circular" };
a.self = a;
console.log(safeStringify(a));
// { "name": "circular", "self": "[Circular]" } WeakSet giữ các tham chiếu mà không ngăn cản garbage collection, điều này tránh các rò rỉ
bộ nhớ trong các tiến trình chạy dài.
Các Giá Trị BigInt
JSON.stringify throw một TypeError cho các giá trị BigInt vì
đặc tả JSON không có kiểu số nguyên 64-bit. Chuyển thành chuỗi trong replacer của bạn:
const data = { id: 9007199254740993n, value: 42 };
JSON.stringify(data, (key, value) =>
typeof value === "bigint" ? value.toString() : value
, 2);
// { "id": "9007199254740993", "value": 42 } Các Giá Trị Map Và Set
Map tuần tự hóa thành một object rỗng và Set tuần tự hóa thành một mảng rỗng —
không phải nội dung của chúng. Chuyển chúng một cách rõ ràng trong một replacer:
const data = {
tags: new Set(["json", "javascript"]),
meta: new Map([["source", "api"]]),
};
JSON.stringify(data, (key, value) => {
if (value instanceof Set) return [...value];
if (value instanceof Map) return Object.fromEntries(value);
return value;
}, 2);
// { "tags": ["json", "javascript"], "meta": { "source": "api" } } Các Giá Trị undefined
Các thuộc tính object với giá trị undefined bị âm thầm loại bỏ. Các vị trí mảng với
undefined trở thành null. Dùng một replacer để chuyển undefined
thành null khi bạn cần bảo toàn tất cả các khóa:
const obj = { a: 1, b: undefined, c: null };
JSON.stringify(obj, null, 2);
// { "a": 1, "c": null } — "b" bị âm thầm loại bỏ
// Khắc phục:
JSON.stringify(obj, (key, value) =>
value === undefined ? null : value
, 2);
// { "a": 1, "b": null, "c": null } Định Dạng JSON Tức Thì Trong Trình Duyệt
Nếu bạn cần định dạng một khối JSON ngay bây giờ mà không viết bất kỳ code nào, Toova JSON Formatter xử lý nó trong một cú nhấp — dán JSON thô, nhận đầu ra được in đẹp với thụt dòng 2 hoặc 4 khoảng trắng, và sao chép kết quả. Không đăng ký, không tải tệp, mọi thứ chạy cục bộ trong trình duyệt của bạn.
Để chuyển đổi giữa các định dạng, JSON to YAML và JSON to CSV theo cùng cách tiếp cận ưu tiên quyền riêng tư — dữ liệu của bạn không bao giờ rời thiết bị.
Kết Luận
Đối với hầu hết các trường hợp sử dụng, JSON.stringify(obj, null, 2) là tất cả những gì bạn cần. Thêm một hàm replacer
khi bạn cần lọc, ẩn hoặc khóa đã sắp xếp. Bọc JSON.parse trong một
try/catch khi xử lý đầu vào bên ngoài. Tiếp cận streaming chỉ khi kích thước tệp làm cho phân tích đồng bộ
không thực tế. Và giữ các trường hợp biên — tham chiếu vòng tròn, BigInt,
Map/Set, undefined — trong đầu khi làm việc
với các hình dạng dữ liệu bất thường.