Mengelola JSON untuk Konfigurasi, Fixture, dan Data Lokal
Intinya: Konfigurasi harus berlapis dan divalidasi; fixture harus deterministik, terbaca, dan bisa di-diff. Jika dua jenis file ini dikelola dengan buruk, biaya troubleshooting tetap tinggi untuk waktu lama.

Intinya: bangun konfigurasi sebagai default, environment, dan override lokal dengan validasi saat startup sebelum mengkhawatirkan format file; buat fixture deterministik dalam waktu, ID, urutan key, dan indentasi agar tetap bisa di-diff, dan baru setelah itu otomatiskan masking.
Dua jenis JSON ini tidak ikut serta dalam logika bisnis, tetapi mereka menentukan "berapa lama rekan baru bisa mulai berjalan" dan "berapa lama menemukan masalah produksi".
1. Konfigurasi: struktur tiga lapis
| Lapisan | Sumber | Dikelola oleh | Di-commit |
|---|---|---|---|
| Default | Konstanta di kode / default.json |
Developer | Ya |
| Konfigurasi environment | config/production.json, environment variable |
Operasi | Ya (tanpa rahasia) |
| Override lokal | .env.local, config/local.json |
Individu | Tidak (gitignored) |
Urutan merge: default → environment → lokal, dengan setiap lapisan berikutnya menimpa yang sebelumnya. Merge hanya se-dalam yang dibutuhkan bisnis; jangan melakukan deep merge secara buta — deep merge membuat masalah seperti "array tiba-tiba tergabung" sangat sulit ditemukan.
/**
* Menggabungkan lapisan konfigurasi menurut prioritas; lapisan berikutnya menimpa yang sebelumnya, dan array diganti utuh alih-alih digabung.
* @param layers Lapisan konfigurasi dari prioritas terendah ke tertinggi
* @returns Object konfigurasi hasil merge
*/
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;
}
// Array dan skalar selalu diganti utuh untuk menghindari penggabungan elemen yang tidak disengaja
mergedConfig[configKey] = configValue;
}
return mergedConfig;
}, {});
}
2. Validasi saat startup, jangan tunggu sampai runtime
Biaya kesalahan konfigurasi tumbuh eksponensial seiring betapa terlambatnya ia ditemukan:
| Kapan ditemukan | Biaya |
|---|---|
| Saat startup | Satu baris log, restart dan lanjut |
| Pada request pertama | Sebagian fitur error, satu jam debugging |
| Saat job terjadwal menyala | Alert di tengah malam |
| Pada hari settlement | Insiden rekonsiliasi |
import Ajv2020 from 'ajv/dist/2020';
import configSchema from './config.schema.json' with { type: 'json' };
const ajv = new Ajv2020({ allErrors: true, strict: true });
/**
* Memvalidasi konfigurasi dan langsung melempar error saat gagal, menerapkan fail fast.
* @param rawConfig Konfigurasi hasil merge mentah
* @returns Konfigurasi yang sudah divalidasi
*/
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>;
}
Prinsip pendukung:
- Keluar segera ketika item wajib tidak ada; jangan menyediakan default yang "kelihatan masuk akal" (misalnya menjadikan alamat basis data
localhost); - Keluar segera pada kesalahan tipe; string
"false"bukan booleanfalse, jadi environment variable harus dikonversi secara eksplisit setelah dibaca; - Rahasia tidak pernah masuk repositori; baca hanya dari environment variable atau layanan manajemen rahasia.
3. Format mana yang dipakai untuk file konfigurasi
| File | Format | Alasan |
|---|---|---|
package.json |
JSON standar | Package manager hanya menerima ini |
tsconfig.json |
JSONC | Komentar diizinkan, didukung secara resmi |
.vscode/settings.json |
JSONC | Sama seperti di atas |
| Konfigurasi bisnis | YAML atau JSONC | Butuh komentar menjelaskan arti setiap item |
| Daftar environment variable | .env |
Pasangan key-value datar, mudah diinjeksi |
Uji praktis: apakah file ini akan dibaca oleh alat non-manusia? Jika ya → JSON standar; jika hanya kode Anda sendiri yang membacanya → dialek apa pun boleh.
4. Fixture uji: direktori dan penamaan
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
Konvensi penamaan yang baik adalah object-scenario: order-missing-field jauh lebih berguna daripada test1.json.
Nama file non-ASCII kadang menyebabkan masalah encoding di toolchain lintas platform, jadi pertahankan nama file dalam bahasa Inggris sementara isinya boleh tetap dalam bahasa Tionghoa.
4.1 Tiga jenis kasus, tiga jenis ekspektasi
| Direktori | Ekspektasi | Tujuan |
|---|---|---|
valid/ |
Validasi lolos | Alur utama, pembuatan tipe |
invalid/ |
Validasi gagal, dan error menunjuk field yang diharapkan | Penanganan error, copy pesan |
edge/ |
Perilaku eksplisit (lolos atau gagal tidak masalah, tetapi harus di-assert) | Perlindungan regresi |
JSONTestSuite memakai prefix y_ / n_ / i_ untuk tiga jenis yang sama (harus menerima / harus menolak / ditentukan implementasi),
dan Anda bisa meminjam konvensi penamaan itu langsung.
5. Determinisme: buat fixture bisa di-diff
Fixture yang tidak stabil membuat code review tidak berguna:
| Sumber noise | Cara menanganinya |
|---|---|
| Waktu saat ini | Kunci ke konstanta, mis. 2026-01-01T00:00:00Z |
| ID acak / UUID | Gunakan urutan bertambah atau nilai tetap |
| Urutan key | Urutkan berdasarkan abjad saat pembuatan |
| Format float | Seragamkan dua desimal atau konversikan ke string |
| Zona waktu | Gunakan UTC di mana-mana |
| Indentasi | Tetapkan 2 spasi, diperiksa Prettier di CI |
/**
* Mengurutkan key secara rekursif untuk menghasilkan teks JSON yang stabil dan bisa di-diff.
* @param value Nilai yang akan diserialkan
* @param indentWidth Lebar indentasi
* @returns String JSON dengan urutan yang stabil
*/
function toStableJson(value: unknown, indentWidth = 2): string {
/**
* Mengurutkan key object secara rekursif.
* @param input Nilai JSON apa pun
* @returns Nilai baru dengan key terurut
*/
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. Masking dan kepatuhan
Data nyata tidak bisa langsung dipakai sebagai fixture:
- Nama → placeholder seperti "John Doe" dan "Jane Roe";
- Nomor telepon → nomor yang jelas fiktif seperti
13800138000; - Nomor kartu identitas → nomor palsu dengan format benar tetapi check digit tidak valid;
- Alamat → pertahankan struktur provinsi/kota, buang nomor rumah;
- Jumlah uang → pertahankan orde besaran, tetapi jangan pernah mempertahankan informasi rekening nyata.
Sebaiknya tim punya scripts/mask-fixture.mjs yang berjalan atas sampel produksi hasil ekspor untuk menghasilkan versi tersensor,
menghindari "ubah tiga tempat dengan tangan, lupa sisanya".
7. Migrasi dan versi
Fixture juga menjadi basi. Tambahkan field schemaVersion ke fixture, atau letakkan file VERSION di direktori:
{
"schemaVersion": 3,
"id": "1001",
"amount": "89.50"
}
Ketika kontrak API ditingkatkan:
- Tambahkan direktori fixture baru (
v3/); jangan memodifikasi fixture lama di tempatnya; - Pertahankan fixture lama untuk regresi sampai versi API lama dipensiunkan;
- Jalankan kedua versi di CI untuk memastikan kompatibilitas mundur.
8. Checklist
- [ ] Struktur konfigurasi tiga lapis jelas, dan override lokal tidak di-commit;
- [ ] Validasi terjadi saat startup, dan kegagalan langsung keluar;
- [ ] Rahasia hanya berasal dari environment variable; file konfigurasi hanya berisi placeholder;
- [ ] Fixture dipisah ke direktori
valid/invalid/edge, dan nama menyatakan skenarionya; - [ ] Fixture deterministik (waktu, ID, dan urutan tetap) dan bisa di-diff;
- [ ] Data nyata sudah diproses oleh skrip masking;
- [ ] Peningkatan kontrak menambahkan direktori versi baru alih-alih mengedit di tempat.
FAQ
Bagaimana lapisan konfigurasi sebaiknya diurutkan dan di-merge?
Gunakan tiga lapisan — default, konfigurasi environment, dan override lokal — dengan merge default → environment → lokal, di mana setiap lapisan berikutnya menimpa yang sebelumnya. Merge hanya se-dalam yang dibutuhkan bisnis dan ganti array serta skalar secara utuh, agar deep merge tidak menggabungkan array secara tak terduga. Gitignore file override lokal alih-alih meng-commit pengaturan pribadi.
Mengapa konfigurasi harus divalidasi saat startup?
Biaya kesalahan konfigurasi tumbuh eksponensial seiring betapa terlambatnya ia ditemukan: satu baris log saat startup versus insiden rekonsiliasi pada hari settlement. Validasi terhadap JSON Schema saat startup dan keluar segera pada item wajib yang hilang atau kesalahan tipe, alih-alih menyediakan default yang "kelihatan masuk akal".
Bolehkah saya memakai data produksi langsung sebagai fixture uji?
Tidak. Nama, nomor telepon, nomor kartu identitas, alamat, dan jumlah uang semuanya butuh placeholder atau nilai yang jelas fiktif, dan tim sebaiknya punya scripts/mask-fixture.mjs yang berjalan atas ekspor produksi untuk menghasilkan versi tersensor, menghindari pengeditan tangan yang melewatkan sesuatu.