Quản lý JSON cho cấu hình, fixture và dữ liệu cục bộ
Kết luận: 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.

Tóm lại: hãy xây cấu hình thành mặc định, môi trường và ghi đè cục bộ, kèm kiểm chứng lúc khởi động, trước khi bận tâm đến định dạng tệp; làm fixture tất định về thời gian, ID, thứ tự khoá và thụt lề để chúng luôn so sánh được diff, rồi mới tự động hoá việc che dữ liệu.
Hai loại JSON này không tham gia logic nghiệp vụ, nhưng chúng quyết định "đồng nghiệp mới mất bao lâu để chạy được" và "mất bao lâu để lần ra một sự cố trên production".
1. Cấu hình: cấu trúc ba tầng
| Tầng | Nguồn | Người duy trì | Có commit |
|---|---|---|---|
| Mặc định | Hằng số trong mã / default.json |
Lập trình viên | Có |
| Cấu hình môi trường | config/production.json, biến môi trường |
Vận hành | Có (không kèm bí mật) |
| Ghi đè cục bộ | .env.local, config/local.json |
Cá nhân | Không (bị gitignore) |
Thứ tự hợp nhất: mặc định → môi trường → cục bộ, mỗi tầng sau ghi đè tầng trước. Chỉ hợp nhất sâu đúng mức nghiệp vụ cần; đừng hợp nhất sâu một cách mù quáng — hợp nhất sâu khiến những vấn đề như "mảng bị nối ngoài ý muốn" cực kỳ khó phát hiện.
/**
* Merges configuration layers by priority; later layers override earlier ones, and arrays are replaced wholesale rather than concatenated.
* @param layers Configuration layers from lowest to highest priority
* @returns The merged configuration object
*/
function mergeConfigLayers(layers: Array<Record<string, unknown>>): Record<string, unknown> {
return layers.reduce<Record<string, unknown>>((mergedConfig, currentLayer) => {
for (const [configKey, configValue] of Object.entries(currentLayer)) {
const isPlainObject =
typeof configValue === 'object' && configValue !== null && !Array.isArray(configValue);
const existingValue = mergedConfig[configKey];
if (isPlainObject && typeof existingValue === 'object' && existingValue !== null) {
mergedConfig[configKey] = mergeConfigLayers([
existingValue as Record<string, unknown>,
configValue as Record<string, unknown>,
]);
continue;
}
// Arrays and scalars are always replaced wholesale to avoid accidental element concatenation
mergedConfig[configKey] = configValue;
}
return mergedConfig;
}, {});
}
2. Kiểm chứng lúc khởi động, đừng đợi đến lúc chạy
Chi phí của một lỗi cấu hình tăng theo cấp số nhân với độ muộn của việc phát hiện:
| Thời điểm phát hiện | Chi phí |
|---|---|
| Lúc khởi động | Một dòng log, khởi động lại là xong |
| Ở request đầu tiên | Vài tính năng lỗi, một giờ gỡ rối |
| Khi tác vụ định kỳ chạy | Báo động giữa đêm |
| Vào ngày đối soát | Một sự cố đối soát |
import Ajv2020 from 'ajv/dist/2020';
import configSchema from './config.schema.json' with { type: 'json' };
const ajv = new Ajv2020({ allErrors: true, strict: true });
/**
* Validates the configuration and throws immediately on failure, implementing fail fast.
* @param rawConfig The raw merged configuration
* @returns The validated configuration
*/
export function assertConfig(rawConfig: unknown): Record<string, unknown> {
const validate = ajv.compile(configSchema);
if (!validate(rawConfig)) {
const messages = (validate.errors ?? []).map(
(errorItem) => `${errorItem.instancePath || '/'} ${errorItem.message ?? ''}`,
);
throw new Error(`配置校验失败:\n${messages.join('\n')}`);
}
return rawConfig as Record<string, unknown>;
}
Nguyên tắc hỗ trợ:
- Thoát ngay khi thiếu một mục bắt buộc; đừng cung cấp giá trị mặc định "trông có vẻ hợp lý" (chẳng hạn mặc định địa chỉ cơ sở dữ liệu là
localhost); - Thoát ngay khi có lỗi kiểu; chuỗi
"false"không phải booleanfalse, nên biến môi trường phải được chuyển đổi tường minh sau khi đọc; - Bí mật không bao giờ vào kho mã; chỉ đọc chúng từ biến môi trường hoặc dịch vụ quản lý bí mật.
3. Dùng định dạng nào cho tệp cấu hình
| Tệp | Định dạng | Lý do |
|---|---|---|
package.json |
JSON chuẩn | Trình quản lý gói chỉ chấp nhận định dạng này |
tsconfig.json |
JSONC | Cho phép chú thích, được hỗ trợ chính thức |
.vscode/settings.json |
JSONC | Như trên |
| Cấu hình nghiệp vụ | YAML hoặc JSONC | Cần chú thích giải thích ý nghĩa từng mục |
| Danh sách biến môi trường | .env |
Cặp khoá-giá trị phẳng, dễ chèn |
Một phép thử thực tế: tệp này có được công cụ không phải con người đọc không? Nếu có → JSON chuẩn; nếu chỉ mã của bạn đọc → dùng biến thể cũng được.
4. Fixture kiểm thử: thư mục và cách đặt tên
tests/fixtures/
├─ valid/
│ ├─ order-minimal.json
│ ├─ order-full.json
│ └─ order-with-null-fields.json
├─ invalid/
│ ├─ order-missing-id.json
│ ├─ order-price-as-number.json
│ └─ order-nested-broken.json
└─ edge/
├─ big-integer.json
├─ emoji-and-surrogate.json
└─ deep-nesting-64.json
Quy ước đặt tên tốt là đối-tượng-tình-huống: order-missing-field hữu ích hơn nhiều so với test1.json.
Tên tệp không phải ASCII đôi khi gây vấn đề mã hoá trong chuỗi công cụ đa nền tảng, nên giữ tên tệp bằng tiếng Anh trong khi nội dung có thể giữ tiếng Trung.
4.1 Ba loại ca, ba loại kỳ vọng
| Thư mục | Kỳ vọng | Mục đích |
|---|---|---|
valid/ |
Kiểm chứng đạt | Luồng chính, sinh kiểu |
invalid/ |
Kiểm chứng thất bại, và lỗi chỉ đúng trường mong đợi | Xử lý lỗi, nội dung thông báo |
edge/ |
Hành vi tường minh (đạt hay không đều được, nhưng phải được khẳng định) | Bảo vệ hồi quy |
JSONTestSuite dùng tiền tố y_ / n_ / i_ cho đúng ba loại này (phải chấp nhận / phải từ chối / tuỳ triển khai),
và bạn có thể mượn thẳng quy ước đặt tên đó.
5. Tính tất định: làm fixture so sánh diff được
Fixture không ổn định khiến review mã vô nghĩa:
| Nguồn nhiễu | Cách xử lý |
|---|---|
| Thời gian hiện tại | Cố định thành hằng, ví dụ 2026-01-01T00:00:00Z |
| ID ngẫu nhiên / UUID | Dùng một dãy tăng dần hoặc giá trị cố định |
| Thứ tự khoá | Sắp xếp theo bảng chữ cái lúc sinh |
| Định dạng số thực | Thống nhất giữ hai chữ số thập phân hoặc chuyển thành chuỗi |
| Múi giờ | Dùng UTC ở mọi nơi |
| Thụt lề | Cố định 2 dấu cách, được Prettier kiểm tra trong CI |
/**
* Recursively sorts keys to produce stable, diffable JSON text.
* @param value The value to serialize
* @param indentWidth Indentation width
* @returns A JSON string with a stable order
*/
function toStableJson(value: unknown, indentWidth = 2): string {
/**
* Recursively sorts object keys.
* @param input Any JSON value
* @returns A new value with sorted keys
*/
const sortKeysDeep = (input: unknown): unknown => {
if (Array.isArray(input)) {
return input.map((item) => sortKeysDeep(item));
}
if (typeof input === 'object' && input !== null) {
return Object.fromEntries(
Object.keys(input)
.sort()
.map((entryKey) => [entryKey, sortKeysDeep((input as Record<string, unknown>)[entryKey])]),
);
}
return input;
};
return JSON.stringify(sortKeysDeep(value), null, indentWidth);
}
6. Che dữ liệu và tuân thủ
Dữ liệu thật không thể dùng trực tiếp làm fixture:
- Tên người → ký tự giữ chỗ như "John Doe" và "Jane Roe";
- Số điện thoại → số hiển nhiên hư cấu như
13800138000; - Số CMND/CCCD → số giả đúng định dạng nhưng sai chữ số kiểm tra;
- Địa chỉ → giữ cấu trúc tỉnh/thành, bỏ số nhà;
- Số tiền → giữ bậc độ lớn, nhưng không bao giờ giữ thông tin tài khoản thật.
Tốt nhất là nhóm có một scripts/mask-fixture.mjs chạy trên một mẫu production đã xuất để sinh phiên bản đã che,
tránh chuyện "sửa tay ba chỗ, quên phần còn lại".
7. Di trú và phiên bản
Fixture cũng trở nên lỗi thời. Thêm trường schemaVersion vào fixture, hoặc đặt một tệp VERSION trong thư mục:
{
"schemaVersion": 3,
"id": "1001",
"amount": "89.50"
}
Khi hợp đồng API được nâng cấp:
- Thêm thư mục fixture mới (
v3/); đừng sửa fixture cũ tại chỗ; - Giữ fixture cũ để kiểm tra hồi quy cho đến khi phiên bản API cũ bị loại bỏ;
- Chạy cả hai phiên bản trong CI để xác nhận tương thích ngược.
8. Danh sách kiểm tra
- [ ] Cấu trúc cấu hình ba tầng rõ ràng, và phần ghi đè cục bộ không bị commit;
- [ ] Kiểm chứng diễn ra lúc khởi động, và thất bại thì thoát ngay;
- [ ] Bí mật chỉ đến từ biến môi trường; tệp cấu hình chỉ chứa ký tự giữ chỗ;
- [ ] Fixture được chia thành các thư mục
valid/invalid/edge, và tên thể hiện tình huống; - [ ] Fixture tất định (thời gian, ID và thứ tự cố định) và so sánh diff được;
- [ ] Dữ liệu thật đã được script che dữ liệu xử lý;
- [ ] Nâng cấp hợp đồng thì thêm thư mục phiên bản mới thay vì sửa tại chỗ.
FAQ
Nên sắp thứ tự và hợp nhất các tầng cấu hình như thế nào?
Dùng ba tầng — mặc định, cấu hình môi trường và ghi đè cục bộ — hợp nhất theo thứ tự mặc định → môi trường → cục bộ, mỗi tầng sau ghi đè tầng trước. Chỉ hợp nhất sâu đúng mức nghiệp vụ cần và thay thế toàn bộ mảng cùng giá trị vô hướng, để việc hợp nhất sâu không nối mảng ngoài ý muốn. Đưa tệp ghi đè cục bộ vào gitignore thay vì commit thiết lập cá nhân.
Vì sao phải kiểm chứng cấu hình lúc khởi động?
Chi phí của một lỗi cấu hình tăng theo cấp số nhân với độ muộn của việc phát hiện: một dòng log lúc khởi động so với một sự cố đối soát vào ngày đối soát. Hãy kiểm chứng theo JSON Schema lúc khởi động và thoát ngay khi thiếu mục bắt buộc hoặc gặp lỗi kiểu, thay vì cung cấp giá trị mặc định "trông có vẻ hợp lý".
Có nên dùng thẳng dữ liệu production làm fixture kiểm thử?
Không. Tên người, số điện thoại, số CMND/CCCD, địa chỉ và số tiền đều cần ký tự giữ chỗ hoặc giá trị hiển nhiên hư cấu, và nhóm nên có một scripts/mask-fixture.mjs chạy trên bản xuất production để sinh phiên bản đã che, tránh việc sửa tay bỏ sót chỗ này chỗ kia.