JSON Toolkit

คำถามที่พบบ่อยเรื่องการทำงานเป็นทีม: contract, config และ fixture

สรุปสำคัญ: สไตล์การตั้งชื่อ, รูปแบบเวลา, ความหมายของ null, รูปแบบของ enum, รูปแบบ config, การจัดระเบียบ fixture และ merge conflict — สิบสองการแลกเปลี่ยนที่ทีมเถียงกันจริง

ระดับกลางอ่านประมาณ 12 นาทีเผยแพร่ 2026-10-30อัปเดต 2026-10-30
ภาพปกคำถามที่พบบ่อยเรื่องการทำงานเป็นทีม

สรุปสั้น ๆ: ตกลงสิ่งเหล่านี้ครั้งเดียวในข้อตกลงของทีม — envelope แบบ {"data":…,"meta":…} ที่ใช้ HTTP status code, สไตล์การตั้งชื่อเดียวทั้งเว็บไซต์, timestamp แบบ RFC 3339 ที่มีเขตเวลา และ enum แบบ string — แล้วพัฒนา API ใหม่ทุกตัวบน contract เดียวกันโดยเพิ่มฟิลด์เท่านั้น ไม่เปลี่ยนหรือลบฟิลด์เดิม

คำถามเหล่านี้ไม่มีคำตอบที่ถูกเพียงคำตอบเดียว แต่ทีมต้องมีคำตอบเดียว — ไม่เช่นนั้น API ใหม่ทุกตัวจะเริ่มเถียงเรื่องเดิมอีกครั้ง

1. Contract ของ API

API ควรคืนข้อมูลโครงสร้างแบบไหน?

รูปแบบที่พบบ่อยสามแบบ แต่ละแบบมีต้นทุน:

รูปแบบ ตัวอย่าง ข้อดี ข้อเสีย
ข้อมูลดิบ [{...}, {...}] เรียบง่าย พา metadata ไม่ได้
envelope คงที่ {"code":0,"message":"ok","data":{...}} จัดการ error ได้สม่ำเสมอ โค้ดธุรกิจซ้ำกับ HTTP status
envelope แบบ resource {"data":{...},"meta":{...},"links":{...}} ขยายต่อได้ ความหมายชัด ซ้อนเพิ่มอีกระดับ

สำหรับ API ธุรกิจภายใน แนะนำ "envelope แบบ resource + HTTP status code ที่บอกผลลัพธ์":

{
  "data": [{ "id": "1", "name": "示例" }],
  "meta": { "total": 128, "page": 2, "pageSize": 20 }
}

สำหรับ error ให้ใช้ HTTP status code ร่วมกับ body ของ error ที่มีโครงสร้าง แทนการซ้อน code อีกชั้น:

{
  "error": {
    "type": "validation_failed",
    "message": "请求参数校验失败",
    "details": [
      { "path": "/email", "reason": "格式不正确" }
    ]
  }
}

จุดสำคัญ: HTTP status code บอกความสำเร็จหรือความล้มเหลวอยู่แล้ว ดังนั้น business code จึงซ้ำซ้อน เมื่อสองระบบอยู่ร่วมกัน ความไม่สอดคล้องอย่าง "201 แต่ code เป็น 500" แทบจะหลีกเลี่ยงไม่ได้

เลี่ยง array เปล่า เมื่อระดับบนสุดเป็น array คุณแนบ pagination, total หรือ cursor ไม่ได้ และการเพิ่มทีหลังหมายถึงต้องแก้ฝั่งผู้เรียกทุกตัว

ชื่อฟิลด์ควรใช้ snake_case หรือ camelCase?

เลือกอย่างใดอย่างหนึ่ง ใช้ทั้งเว็บไซต์ และอย่าปนกันภายในเอกสาร JSON หนึ่งฉบับ

สไตล์ ระบบนิเวศที่ใช้บ่อย ตัวอย่าง
camelCase JavaScript / TypeScript / Java userName, createdAt
snake_case Python / Ruby / Go (บางส่วน) / คอลัมน์ฐานข้อมูล user_name, created_at
kebab-case พบน้อย; key ที่มีขีดกลางเข้าถึงยาก user-name

เกณฑ์:

  1. ใครเป็นผู้ใช้ JSON นี้: ถ้า frontend เป็นหลักให้ camelCase ถ้า data pipeline และบริการ Python เป็นหลักให้ snake_case;
  2. มันแมปกับคอลัมน์ฐานข้อมูลโดยตรงไหม: snake_case ประหยัดชั้นแปลงเมื่อส่งโครงสร้างตารางตรง ๆ;
  3. ความต่อเนื่องของระบบเดิม: คงสิ่งที่ API เดิมใช้ไว้ ความสม่ำเสมอสำคัญกว่าความถูกต้อง

ถ้าจำเป็นต้องแปลงจริง ๆ ให้ทำที่ขอบ (gateway หรือชั้น request ของ frontend) ไม่ใช่กระจายการเรียก toCamelCase ทั่วโค้ดธุรกิจ

ฟิลด์เวลาควรใช้รูปแบบไหน?

ใช้ string แบบ RFC 3339 / ISO 8601 ที่มี offset ของเขตเวลา:

{
  "createdAt": "2026-10-30T11:37:57+08:00",
  "updatedAt": "2026-10-30T03:37:57Z"
}

จุดสำคัญ:

  • ต้องมีเขตเวลา: 2026-10-30 11:37:57 ที่ไม่มีเขตเวลาจะพังในสถานการณ์ข้ามเขตเวลาอย่างแน่นอน;
  • ใช้ Z หรือ offset อย่างสม่ำเสมอ: ทั้งสองถูกต้อง แต่ห้ามปนกันภายใน API หนึ่งตัว;
  • เลี่ยง numeric timestamp: ไม่ว่าจะเป็นวินาทีหรือมิลลิวินาที และจะนับ leap second ด้วยหรือไม่ ทุกทีมต้องตีความใหม่เอง เมื่อต้องกระชับ ให้ใช้จำนวนเต็มมิลลิวินาที แต่ระบุหน่วยให้ชัดในเอกสาร

แนะนำชื่อฟิลด์ที่ลงท้ายด้วย At เช่น createdAt / updatedAt / deletedAt จะเห็นได้ทันทีว่าค่านั้นเป็นจุดในเวลา ไม่ใช่ช่วงเวลา สำหรับช่วงเวลาใช้ timeoutMs, ttlSeconds — ใส่หน่วยไว้ในชื่อ

ค่า null ควรเป็น null หรือละเว้นฟิลด์?

ขึ้นกับว่า "ฟิลด์นี้มีความหมายหรือไม่":

ความหมาย รูปแบบ หมายเหตุ
ฟิลด์มีอยู่แต่ยังไม่มีค่า "name": null บอกชัดว่า "ตรวจแล้วและมันว่าง"
ฟิลด์ไม่เกี่ยวกับ resource นี้ ละเว้น เช่น ผู้ใช้ทั่วไปไม่ควรมี vipExpireAt
การอัปเดตบางส่วน (PATCH) ละเว้น = ไม่เปลี่ยน เฉพาะ null ที่หมายถึง "ล้างค่า" ตรงนี้

กรณีที่สามคือกรณีที่ถูกละเลยบ่อยที่สุด ในคำขอ PATCH:

{ "nickname": null }

อันนี้หมายถึง "ล้างชื่อเล่น" ส่วนการไม่ใส่ nickname ใน request body หมายถึง "อย่าแตะชื่อเล่น" ถ้าคุณถือทั้งสองอย่างเป็น "ล้างค่า" ผู้ใช้ที่แก้ email ก็จะลบชื่อเล่นของตัวเองไปด้วย

ย้ำอีกครั้ง: อย่าใช้ undefined มันไม่ใช่ค่า JSON ที่ถูกต้อง และ JSON.stringify จะตัด key ออกทั้งหมด พฤติกรรมจึงตรงกับ "ละเว้น" ขณะที่โค้ดดูเหมือน "ส่งค่าว่างเข้ามาชัดเจน"

ฟิลด์ boolean ควรตั้งชื่ออย่างไร?

ใช้คำนำหน้า is / has / can / should และเลี่ยงการปฏิเสธ

{
  "isActive": true,
  "hasPassword": false,
  "canEdit": true
}

บทเรียนสามข้อ:

  1. ห้ามใช้การปฏิเสธ: isDisabled: false บังคับให้ผู้อ่านต้องกลับค่าหนึ่งครั้ง; isEnabled: true ตรงกว่า การปฏิเสธซ้อน (isNotDisabled) เป็นหายนะ;
  2. อย่าใช้ตัวเลขแทน boolean: "enabled": 1 ทำให้ผู้ใช้ทุกคนต้องเขียน === 1 หนึ่งครั้ง;
  3. อย่าใช้ string: "enable": "true" เป็น truthy ใน JavaScript และ if (data.enable) ก็เป็นจริงกับ "false" ด้วย; bug แบบนี้ตามรอยยากมาก

ฟิลด์ enum ควรใช้ตัวเลขหรือ string?

ใช้ string เว้นแต่มีข้อจำกัดเรื่องพื้นที่จัดเก็บหรือแบนด์วิดท์ชัดเจน

{ "status": "active" }

เทียบกับ { "status": 1 } string ชนะเพราะ:

  • log, ไฟล์ packet capture และฐานข้อมูลอ่านได้ตรง ๆ โดยไม่ต้องเปิดตารางแมป;
  • การเพิ่มค่า enum ไม่เปลี่ยนความหมายของค่าที่มีอยู่ (การแทรกค่าใน enum แบบตัวเลขจะเลื่อนทุกอย่าง);
  • frontend ไม่ต้องดูแลแมป 1 → 'active' ให้ตรงกัน

ต้นทุนคือไบต์ที่เพิ่มขึ้นเล็กน้อย ซึ่งแทบไม่มีความหมายหลังบีบอัด

แนวปฏิบัติคู่กัน: ใช้ตัวพิมพ์เล็ก + ขีดล่างสำหรับค่า enum แบบ string (pending_review) และจำกัดด้วย enum ใน Schema เพื่อให้ค่าที่ไม่ถูกต้องถูกปฏิเสธในขั้น validation

2. Configuration และ fixture

config ควรเป็น JSON, YAML หรือ TOML?

เลือกตามวัตถุประสงค์ ไม่ใช่ตามความชอบ:

รูปแบบ เหมาะกับ ไม่เหมาะกับ
JSON การอ่านเขียนด้วยโปรแกรม, ข้อมูลที่มีโครงสร้างเข้มงวด, validation ด้วย Schema การแก้ด้วยมือบ่อย ๆ (ไม่ให้ comment)
YAML config การ deploy ที่ซ้อนลึก (K8s) การย่อหน้าเขียนมือพังง่าย; yes/no กลายเป็น boolean
TOML config ของ dependency และเครื่องมือที่ไม่ซ้อนลึก (Cargo.toml, pyproject.toml) การซ้อนลึกอ่านยาก
JSONC config ของ editor ที่ต้องมี comment (tsconfig.json) การข้ามขอบเขตระบบ

ข้อผิดพลาดที่พบบ่อยในทีมคือเก็บ config ธุรกิจเป็น YAML — การแปลงชนิดโดยนัยของ YAML (1.0 กลายเป็น number, NO กลายเป็น boolean, 2026-10-30 กลายเป็น date) ให้ผลต่างกันในแต่ละ parser

ถ้า config ต้องถูกตรวจอย่างเข้มงวดด้วยโปรแกรม ให้เลือก JSON + Schema และอธิบาย comment ผ่านฟิลด์ _comment หรือเอกสารแยก

ควรจัดระเบียบ fixture สำหรับทดสอบอย่างไร?

จัดตาม "baseline หนึ่งชุด + diff น้อยที่สุด" ไม่ใช่คัดลอก JSON ก้อนใหญ่สำหรับทุกกรณีทดสอบ

fixtures/
├─ users/
│  ├─ base.json              # object ผู้ใช้พื้นฐาน
│  ├─ no-email.json          # ขาดเฉพาะ email
│  └─ invalid-age.json       # เฉพาะ age ที่นอกช่วง
└─ fixtures.ts               # ฟังก์ชันช่วยโหลดและ merge แบบตื้น
import baseUser from './users/base.json';

export function makeUser(overrides: Partial<User> = {}): User {
  return { ...baseUser, ...overrides };
}

ด้วยวิธีนี้กรณีทดสอบใหม่เขียนแค่ฟิลด์ที่ต่าง และการเปลี่ยนฟิลด์ใน baseline จะอัปเดตทุกกรณีโดยอัตโนมัติ

กฎสามข้อ:

  1. fixture ต้องผ่าน Schema validation: fixture baseline เองก็ควรเป็นข้อมูลที่ถูกต้อง;
  2. อย่าส่งข้อมูลจริงจาก production ออกมา: เบอร์โทร, เลขบัตรประชาชน และที่อยู่ล้วนเป็นข้อมูลส่วนบุคคล;
  3. fixture ควรอยู่ใน version control แต่ต้องเล็ก: ไฟล์เดียวที่เกินหลายสิบ KB สมควรถูกตัดหรือสร้างขึ้นมาใหม่

จะลด merge conflict ในไฟล์ JSON ได้อย่างไร?

JSON ขัดแย้งกันง่าย: formatter จะจัดรูปแบบใหม่ทั้งไฟล์ในครั้งเดียว สองคนที่แต่ละคนแก้คนละบรรทัดจึงขัดกันทั้งไฟล์

กลยุทธ์สี่ข้อ:

  1. รวมรูปแบบให้เป็นหนึ่งเดียวและใส่ใน .editorconfig: การย่อหน้า, การขึ้นบรรทัดใหม่ และบรรทัดใหม่ท้ายไฟล์ที่สม่ำเสมอช่วยเลี่ยง diff ที่ไม่เกี่ยวกัน;
  2. เรียง key: ตกลงลำดับตัวอักษร เพื่อให้ฟิลด์ใหม่ไม่ชนกับจุดแทรกของคนอื่น;
  3. แยกไฟล์: แบ่ง config.json ก้อนใหญ่เป็นหลายไฟล์ตามโดเมน พื้นที่ขัดแย้งก็เล็กลงเอง;
  4. เลี่ยง commit แบบ "จัดรูปแบบใหม่ทั้งหมด": แยกการจัดรูปแบบกับการเปลี่ยนธุรกิจเป็นสอง commit เพื่อให้ review อ่านได้

ถ้าทีมใช้ lock file (pnpm-lock.yaml, package-lock.json) อย่า merge ด้วยมือ ให้สร้างใหม่แทน:

git checkout --theirs pnpm-lock.yaml
pnpm install

จะรักษาความเข้ากันได้ระหว่างเวอร์ชันได้อย่างไร?

หลักการคือ "เพิ่ม ไม่เปลี่ยน ไม่ลบ"

การเปลี่ยนแปลง ความเข้ากันได้ แนวทาง
เพิ่มฟิลด์ทางเลือก เข้ากันได้ เพิ่มได้ ผู้ใช้จะเพิกเฉยฟิลด์ที่ไม่รู้จัก
เพิ่มฟิลด์ที่จำเป็น ไม่เข้ากัน ขึ้นเวอร์ชัน หรือให้ค่าเริ่มต้น
ลบฟิลด์ ไม่เข้ากัน ประกาศ deprecated ก่อน ดูผู้เรียก แล้วจึงลบ
เปลี่ยนชนิดของฟิลด์ ไม่เข้ากัน เพิ่มชื่อฟิลด์ใหม่และเขียนคู่กันในช่วงเปลี่ยนผ่าน
เปลี่ยนชุดค่า enum ขึ้นกับกรณี การเพิ่มค่าเข้ากันได้ การลบไม่

กลไกสนับสนุน:

  • ผู้ใช้ฝั่งปลายทางอ่านแบบ "ผ่อนปรน": เพิกเฉยฟิลด์ที่ไม่รู้จักและให้ค่าเริ่มต้นกับฟิลด์ที่หายไป;
  • เซิร์ฟเวอร์เก็บฟิลด์เก่าไว้ระยะหนึ่งและคืน header Deprecation ใน response เป็นคำใบ้;
  • ใน Schema อย่าทำเครื่องหมายฟิลด์ใหม่เป็น required และติดแท็กฟิลด์ที่เลิกใช้ด้วย deprecated: true

จะตรวจสอบ contract ใน CI ได้อย่างไร?

ถือ Schema เป็นส่วนหนึ่งของโค้ดและตรวจทุก commit

# ตรวจข้อมูลตัวอย่างกับ Schema (ใช้ ajv-cli)
npx ajv-cli validate -s schemas/user.json -d "fixtures/users/*.json" --strict=false

รันสามอย่างใน CI:

  1. ความถูกต้องของ Schema เอง: การประกาศ $schema ถูกต้องหรือไม่ และ $ref resolve ได้หรือไม่;
  2. fixture ตรงกับ Schema หรือไม่: ยืนยันว่าข้อมูลทดสอบไม่คลาดเคลื่อน;
  3. contract เข้ากันได้ย้อนหลังหรือไม่: เทียบกับเวอร์ชันก่อนหน้าและตรวจฟิลด์ที่ถูกลบหรือชนิดที่เปลี่ยน

ข้อที่สามต้องมีเครื่องมือสนับสนุน (เช่น openapi-diff หรือ json-schema-diff) ถ้าไม่มีอย่างน้อยให้ใช้สคริปต์เทียบการเปลี่ยนแปลงใน array required และชุด key ใน properties

3. สรุปเร็ว

คำถาม ข้อแนะนำ
โครงสร้าง response {"data":..., "meta":...} และใช้ HTTP status code สำหรับ error
สไตล์การตั้งชื่อ camelCase (เน้น frontend) หรือ snake_case (เน้นข้อมูล) ใช้ให้สม่ำเสมอทั้งเว็บไซต์
รูปแบบเวลา string แบบ RFC 3339 ที่มีเขตเวลา ชื่อฟิลด์ลงท้ายด้วย At
ค่า null ใช้ null เมื่อไม่มีค่า ละเว้นฟิลด์ที่ไม่เกี่ยวข้อง และการละเว้นใน PATCH หมายถึง "ไม่เปลี่ยน"
ชื่อ boolean คำนำหน้า is/has/can ไม่มีการปฏิเสธ ไม่ใช้ 0/1
enum string ตัวพิมพ์เล็กคั่นด้วยขีดล่าง และเพิ่ม enum ใน Schema
รูปแบบ config JSON สำหรับ validation ด้วยโปรแกรม, TOML หรือ JSONC สำหรับแก้ด้วยมือ
fixture baseline หนึ่งชุด + override ที่ diff น้อยที่สุด และต้องถูกต้องตาม Schema
merge conflict รวมรูปแบบ, เรียง key, แยกไฟล์
ความเข้ากันได้ระหว่างเวอร์ชัน เพิ่ม ไม่เปลี่ยน ไม่ลบ; ประกาศ deprecated ก่อนลบ