JSON Toolkit

การจัดการ JSON สำหรับคอนฟิก fixture และข้อมูลภายในเครื่อง

สรุปสำคัญ: คอนฟิกต้องถูกแบ่งชั้นและตรวจสอบ; fixture ต้องแน่นอน อ่านง่าย และ diff ได้ หากไฟล์สองชนิดนี้ถูกจัดการไม่ดี ต้นทุนการวินิจฉัยปัญหาจะสูงไปอีกนาน

ผู้เริ่มต้นอ่านประมาณ 10 นาทีเผยแพร่ 2026-10-30อัปเดต 2026-10-30
ภาพปกการจัดการ JSON สำหรับคอนฟิก fixture และข้อมูลภายในเครื่อง

สรุปสั้น: สร้างคอนฟิกเป็นค่าเริ่มต้น สภาพแวดล้อม และการโอเวอร์ไรด์ภายในเครื่อง พร้อมการตรวจสอบตอนเริ่มระบบก่อนจะกังวลเรื่องรูปแบบไฟล์; ทำให้ fixture แน่นอนในเรื่องเวลา ID ลำดับคีย์ และการย่อหน้าเพื่อให้ยัง diff ได้ และค่อยทำระบบปกปิดอัตโนมัติ

JSON สองชนิดนี้ไม่ได้มีส่วนร่วมในตรรกะธุรกิจ แต่พวกมันกำหนดว่า "เพื่อนใหม่ต้องใช้เวลานานแค่ไหนจึงจะเริ่มทำงานได้" และ "ต้องใช้เวลานานแค่ไหนจึงจะหาปัญหาใน production เจอ"

1. คอนฟิก: โครงสร้างสามชั้น

ชั้น แหล่งที่มา ดูแลโดย commit หรือไม่
ค่าเริ่มต้น ค่าคงที่ในโค้ด / default.json นักพัฒนา ใช่
คอนฟิกสภาพแวดล้อม config/production.json, environment variable ฝ่ายปฏิบัติการ ใช่ (ไม่มีความลับ)
การโอเวอร์ไรด์ภายในเครื่อง .env.local, config/local.json รายบุคคล ไม่ (gitignore)

ลำดับการ merge: ค่าเริ่มต้น → สภาพแวดล้อม → ภายในเครื่อง โดยแต่ละชั้นถัดไปโอเวอร์ไรด์ชั้นก่อนหน้า merge ลึกเท่าที่ ธุรกิจต้องการ เท่านั้น; อย่า deep-merge แบบไม่ลืมหูลืมตา — การ deep merge ทำให้ปัญหาเช่น "อาร์เรย์ถูกต่อกันโดยไม่คาดคิด" หาเจอยากมาก

/**
 * รวมชั้นคอนฟิกตามลำดับความสำคัญ; ชั้นหลังโอเวอร์ไรด์ชั้นก่อน และอาร์เรย์ถูกแทนที่ทั้งหมดไม่ใช่ต่อกัน
 * @param layers ชั้นคอนฟิกจากความสำคัญต่ำสุดไปสูงสุด
 * @returns ออบเจ็กต์คอนฟิกที่ 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;
      }

      // อาร์เรย์และค่าสเกลาถูกแทนที่ทั้งหมดเสมอ เพื่อหลีกเลี่ยงการต่อสมาชิกโดยไม่ตั้งใจ
      mergedConfig[configKey] = configValue;
    }
    return mergedConfig;
  }, {});
}

2. ตรวจสอบตอนเริ่มระบบ อย่ารอถึงตอนรัน

ต้นทุนของคอนฟิกที่ผิดเพิ่มขึ้นเป็นเอ็กซ์โพเนนเชียลตามความล่าช้าในการค้นพบ:

ค้นพบเมื่อใด ต้นทุน
ตอนเริ่มระบบ log หนึ่งบรรทัด รีสตาร์ทแล้วไปต่อ
ที่ request แรก บางฟีเจอร์ error ใช้เวลา debug หนึ่งชั่วโมง
เมื่อ scheduled job ทำงาน แจ้งเตือนกลางดึก
ในวันปิดบัญชี เหตุการณ์กระทบยอด
import Ajv2020 from 'ajv/dist/2020';
import configSchema from './config.schema.json' with { type: 'json' };

const ajv = new Ajv2020({ allErrors: true, strict: true });

/**
 * ตรวจสอบคอนฟิกและโยนข้อผิดพลาดทันทีเมื่อล้มเหลว เพื่อให้ fail fast
 * @param rawConfig คอนฟิกดิบที่ merge แล้ว
 * @returns คอนฟิกที่ตรวจสอบแล้ว
 */
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>;
}

หลักการสนับสนุน:

  • ออกทันทีเมื่อรายการที่จำเป็นขาดหาย; อย่าให้ค่าเริ่มต้นที่ "ดูสมเหตุสมผล" (เช่นตั้งค่าที่อยู่ฐานข้อมูลเป็น localhost);
  • ออกทันทีเมื่อชนิดผิด; สตริง "false" ไม่ใช่บูลีน false ดังนั้น environment variable ต้องถูกแปลงอย่างชัดเจนหลังอ่าน;
  • ความลับไม่เข้า repository เด็ดขาด; อ่านจาก environment variable หรือบริการจัดการความลับเท่านั้น

3. ใช้รูปแบบใดสำหรับไฟล์คอนฟิก

ไฟล์ รูปแบบ เหตุผล
package.json JSON มาตรฐาน ตัวจัดการแพ็กเกจรับเฉพาะแบบนี้
tsconfig.json JSONC อนุญาตคอมเมนต์ รองรับอย่างเป็นทางการ
.vscode/settings.json JSONC เหมือนข้างบน
คอนฟิกธุรกิจ YAML หรือ JSONC ต้องมีคอมเมนต์อธิบายความหมายแต่ละรายการ
รายการ environment variable .env คู่คีย์-ค่าแบบแบน ฉีดได้ง่าย

การทดสอบเชิงปฏิบัติ: ไฟล์นี้จะถูกอ่านโดยเครื่องมือที่ไม่ใช่มนุษย์หรือไม่? หากใช่ → JSON มาตรฐาน; หากมีเพียงโค้ดของคุณเองที่อ่าน → ใช้ภาษาถิ่นได้

4. Test fixture: ไดเรกทอรีและการตั้งชื่อ

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

ธรรมเนียมการตั้งชื่อที่ดีคือ object-scenario: order-missing-field มีประโยชน์กว่า test1.json มาก ชื่อไฟล์ที่ไม่ใช่ ASCII บางครั้งทำให้เกิดปัญหาการเข้ารหัสใน toolchain ข้ามแพลตฟอร์ม ดังนั้นให้ชื่อไฟล์เป็นภาษาอังกฤษขณะที่เนื้อหายังเป็นภาษาจีนได้

4.1 กรณีสามชนิด ความคาดหวังสามแบบ

ไดเรกทอรี ความคาดหวัง วัตถุประสงค์
valid/ การตรวจสอบผ่าน เส้นทางหลัก การสร้างชนิด
invalid/ การตรวจสอบล้มเหลว และ error ชี้ไปที่ฟิลด์ที่คาด การจัดการข้อผิดพลาด ข้อความแจ้ง
edge/ พฤติกรรมชัดเจน (ผ่านหรือล้มเหลวก็ได้ แต่ต้อง assert) การป้องกันการถดถอย

JSONTestSuite ใช้คำนำหน้า y_ / n_ / i_ สำหรับสามชนิดเดียวกัน (ควรรับ / ควรปฏิเสธ / ขึ้นกับการใช้งาน) และคุณสามารถยืมธรรมเนียมการตั้งชื่อนั้นไปใช้ได้โดยตรง

5. ความแน่นอน: ทำให้ fixture diff ได้

Fixture ที่ไม่มั่นคงทำให้ code review ไร้ประโยชน์:

แหล่งความรบกวน วิธีรับมือ
เวลาปัจจุบัน ตรึงเป็นค่าคงที่ เช่น 2026-01-01T00:00:00Z
ID สุ่ม / UUID ใช้ลำดับเพิ่มขึ้นหรือค่าคงที่
ลำดับคีย์ เรียงตามตัวอักษรตอนสร้าง
การจัดรูปแบบทศนิยม เก็บสองตำแหน่งอย่างสม่ำเสมอหรือแปลงเป็นสตริง
เขตเวลา ใช้ UTC ทุกที่
การย่อหน้า ตรึงที่ 2 ช่องว่าง ตรวจด้วย Prettier ใน CI
/**
 * เรียงคีย์แบบเรียกซ้ำเพื่อสร้างข้อความ JSON ที่มั่นคงและ diff ได้
 * @param value ค่าที่จะทำให้เป็นอนุกรม
 * @param indentWidth ความกว้างการย่อหน้า
 * @returns สตริง JSON ที่มีลำดับมั่นคง
 */
function toStableJson(value: unknown, indentWidth = 2): string {
  /**
   * เรียงคีย์ของออบเจ็กต์แบบเรียกซ้ำ
   * @param input ค่า JSON ใด ๆ
   * @returns ค่าใหม่ที่มีคีย์เรียงแล้ว
   */
  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. การปกปิดและข้อกำหนดด้านการปฏิบัติตาม

ข้อมูลจริงไม่สามารถใช้เป็น fixture โดยตรงได้:

  • ชื่อ → ตัวแทนเช่น "John Doe" และ "Jane Roe";
  • เบอร์โทร → หมายเลขที่สมมติชัดเจนเช่น 13800138000;
  • เลขบัตรประชาชน → หมายเลขปลอมที่มีรูปแบบถูกต้องแต่หลักตรวจสอบไม่ถูก;
  • ที่อยู่ → คงโครงสร้างจังหวัด/อำเภอ ตัดบ้านเลขที่;
  • จำนวนเงิน → คงลำดับขนาด แต่ไม่เก็บข้อมูลบัญชีจริงเด็ดขาด

ดีที่สุดคือทีมมี scripts/mask-fixture.mjs ที่รันบนตัวอย่าง production ที่ส่งออกมาเพื่อสร้างฉบับที่ปกปิดแล้ว หลีกเลี่ยง "แก้มนุษย์สามที่แล้วลืมที่เหลือ"

7. การย้ายข้อมูลและเวอร์ชัน

Fixture ก็เก่าได้เช่นกัน เพิ่มฟิลด์ schemaVersion ใน fixture หรือวางไฟล์ VERSION ในไดเรกทอรี:

{
  "schemaVersion": 3,
  "id": "1001",
  "amount": "89.50"
}

เมื่อ API contract ถูกอัปเกรด:

  1. เพิ่มไดเรกทอรี fixture ใหม่ (v3/); อย่าแก้ fixture เก่าในที่;
  2. เก็บ fixture เก่าไว้สำหรับการทดสอบถดถอยจนกว่า API เวอร์ชันเก่าจะเลิกใช้;
  3. รันทั้งสองเวอร์ชันใน CI เพื่อยืนยันความเข้ากันได้ย้อนหลัง

8. รายการตรวจสอบ

  • [ ] โครงสร้างคอนฟิกสามชั้นชัดเจน และการโอเวอร์ไรด์ภายในเครื่องไม่ถูก commit;
  • [ ] การตรวจสอบเกิดขึ้นตอนเริ่มระบบ และล้มเหลวแล้วออกทันที;
  • [ ] ความลับมาจาก environment variable เท่านั้น ไฟล์คอนฟิกมีแต่ตัวแทน;
  • [ ] Fixture แยกเป็นไดเรกทอรี valid/invalid/edge และชื่อสื่อถึงสถานการณ์;
  • [ ] Fixture แน่นอน (เวลาคงที่ ID คงที่ ลำดับคงที่) และ diff ได้;
  • [ ] ข้อมูลจริงผ่านสคริปต์ปกปิดแล้ว;
  • [ ] การอัปเกรด contract เพิ่มไดเรกทอรีเวอร์ชันใหม่แทนการแก้ในที่

FAQ

ลำดับชั้นคอนฟิกและวิธี merge ควรเป็นอย่างไร?

ใช้สามชั้น — ค่าเริ่มต้น คอนฟิกสภาพแวดล้อม และการโอเวอร์ไรด์ภายในเครื่อง — merge ค่าเริ่มต้น → สภาพแวดล้อม → ภายในเครื่อง โดยแต่ละชั้นถัดไปโอเวอร์ไรด์ชั้นก่อนหน้า merge ลึกเท่าที่ธุรกิจต้องการและแทนที่อาร์เรย์กับค่าสเกลาทั้งหมด เพื่อไม่ให้ deep merge ต่ออาร์เรย์โดยไม่คาดคิด ให้ gitignore ไฟล์โอเวอร์ไรด์ภายในเครื่องแทนการ commit การตั้งค่าส่วนบุคคล

ทำไมต้องตรวจสอบคอนฟิกตอนเริ่มระบบ?

ต้นทุนของคอนฟิกที่ผิดเพิ่มขึ้นเป็นเอ็กซ์โพเนนเชียลตามความล่าช้าในการค้นพบ: log หนึ่งบรรทัดตอนเริ่มระบบเทียบกับเหตุการณ์กระทบยอดในวันปิดบัญชี ตรวจสอบกับ JSON Schema ตอนเริ่มระบบและออกทันทีเมื่อรายการที่จำเป็นขาดหายหรือชนิดผิด แทนที่จะให้ค่าเริ่มต้นที่ "ดูสมเหตุสมผล"

ควรใช้ข้อมูล production เป็น test fixture โดยตรงหรือไม่?

ไม่ ชื่อ เบอร์โทร เลขบัตรประชาชน ที่อยู่ และจำนวนเงินล้วนต้องใช้ตัวแทนหรือค่าที่สมมติชัดเจน และทีมควรมี scripts/mask-fixture.mjs ที่รันบนข้อมูล production ที่ส่งออกมาเพื่อสร้างฉบับปกปิด หลีกเลี่ยงการแก้ด้วยมือที่พลาดบางจุด