JSON Toolkit

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.

Pemula±10 menit bacaDiterbitkan 2026-10-30Diperbarui 2026-10-30
Sampul mengelola JSON untuk konfigurasi, fixture, dan data lokal

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 boolean false, 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:

  1. Tambahkan direktori fixture baru (v3/); jangan memodifikasi fixture lama di tempatnya;
  2. Pertahankan fixture lama untuk regresi sampai versi API lama dipensiunkan;
  3. 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.