JSON Toolkit

ทำไม JSON Schema ที่สร้างอัตโนมัติจึงปฏิเสธข้อมูลจริงซ้ำ ๆ

สรุปสำคัญ: schema ที่สร้างขึ้นมักบล็อกข้อมูลที่ถูกต้อง เรียนรู้ว่าทำไมตัวอย่างที่สมบูรณ์กว่าจึงให้ schema ที่แย่กว่า แล้วรู้กระบวนการ: การสุ่มหลายเรกคอร์ด การเติมความหมาย การชี้ error ให้ตรงจุด

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

สรุปสั้น: ปรับขอบเขต required/optional ด้วยมือก่อน — ป้อนเรกคอร์ด 5–20 เรกคอร์ดที่มีรูปร่างต่างกันโดยปิด "all required" — แล้วสร้าง schema และเติม format, enum และ additionalProperties ด้วยมือ; ตัวอย่างที่ครบทุกฟิลด์เพียงตัวเดียวจะเข้มงวดเกินไปเสมอ

generator เห็นได้เพียงเรกคอร์ดเดียวที่คุณวางเข้าไป มันอ่าน "เรกคอร์ดนี้มี email" เป็น "ทุกเรกคอร์ดต้องมี email" ยิ่งตัวอย่างของคุณสมบูรณ์เท่าไร มันก็ยิ่งเรียกเรียกร้องมากขึ้นเท่านั้น

1. generator ทำการแมปเชิงโครงสร้าง ไม่ใช่ความเข้าใจธุรกิจ

ยก JSON ก้อนหนึ่งให้ generator แล้วมันทำเพียงสิ่งเดียว: เดินโครงสร้างของค่าและแปลแต่ละโหนดเป็นคีย์เวิร์ดของ schema ตารางการแมปทั้งหมดถูกกำหนดตายตัว:

ค่าในตัวอย่าง ชนิดที่อนุมานได้ หมายเหตุ
"hello" {"type": "string"} ไม่แยกแยะ email, date, UUID
42 {"type": "number"} จำนวนเต็มไม่ได้ถูกอนุมานเป็น integer
3.14 {"type": "number"} เหมือนข้างต้น
true {"type": "boolean"} —
null {"type": "null"} รู้จักเพียง null ไม่เคยรู้จัก "nullable"
[1, "a"] {"type": "array", "items": {"type": ["number", "string"]}} ชนิดของสมาชิกถูกรวมเป็น union
{"a": 1} {"type": "object", "properties": {...}, "required": [...]} required คือจุดสำคัญตรงนี้

ปัญหาทั้งหมดอยู่ที่แถวสุดท้าย required ไม่ได้มาจากชนิด; มันมาจาก "คีย์นี้มีอยู่ในตัวอย่างนี้" เมื่อตัวอย่างเป็นเรกคอร์ดเดียว ข้อความนั้นเท่ากับ "ทุกคีย์บังคับ"

แต่ข้อมูลที่ API จริงส่งกลับมามักมีฟิลด์ครึ่งหนึ่งเป็น optional นั่นคือจุดที่ความขัดแย้งเริ่มต้น

2. ห้าเรกคอร์ดรูปร่างเดียวกัน อัตราการยอมรับห้าระดับ

เริ่มจากการวัด ใครที่ดูห้าเรกคอร์ดด้านล่างก็จะบอกว่า "รูปร่างเดียวกัน":

[
  {
    "id": 1,
    "name": "Ada",
    "email": "ada@example.com",
    "tags": ["a"],
    "score": 10
  },
  { "id": 2, "name": "Bob", "tags": [], "score": 7 },
  {
    "id": 3,
    "name": "Cy",
    "email": "cy@example.com",
    "tags": ["a", "b"],
    "score": 8.5
  },
  { "id": 4, "name": "Dee", "email": null, "tags": ["c"], "score": 9 },
  {
    "id": 5,
    "name": "Eve",
    "email": "eve@example.com",
    "tags": ["d"],
    "score": 6,
    "admin": true
  }
]

email ปรากฏใน 3 จาก 5 เรกคอร์ด admin ปรากฏเพียงครั้งเดียว และเรกคอร์ดที่ 2 มี tags เป็น array ว่าง

ตอนนี้สร้าง schema จากแต่ละเรกคอร์ดแยกกัน แล้วใช้ผลลัพธ์แต่ละตัวตรวจสอบทั้งห้าเรกคอร์ด (validator: ajv 8):

สร้างจาก จำนวนคีย์ required ที่สร้างได้ ยอมรับจาก 5
เรกคอร์ดที่ 1 5 id, name, email, tags, score 3 เรกคอร์ด
เรกคอร์ดที่ 2 4 id, name, tags, score 5 เรกคอร์ด
เรกคอร์ดที่ 3 5 id, name, email, tags, score 3 เรกคอร์ด
เรกคอร์ดที่ 4 5 id, name, email, tags, score 1 เรกคอร์ด
เรกคอร์ดที่ 5 6 id, name, email, tags, score, admin 1 เรกคอร์ด

ไม่มี schema ใดในห้าตัวถูกต้อง แต่ ตัวเดียวที่ยอมรับข้อมูลทั้งหมดมาจากเรกคอร์ดที่ 2 — เรกคอร์ดที่ไม่มี email มันชนะด้วยเหตุผลง่าย ๆ: คีย์ที่ไม่เคยปรากฏจะไม่ถูกเขียนลง required

เรกคอร์ดที่ 4 ให้บทเรียนมากกว่านั้นอีก: ค่า email ของมันเป็น null generator จึง hardcode {"type": "null"} และอีกสี่เรกคอร์ดที่มี email เป็นสตริงถูกปฏิเสธทั้งหมด

3. ทำไม "วางเรกคอร์ดที่สมบูรณ์ที่สุด" จึงเป็นตัวเลือกที่แย่ที่สุด

สัญชาตญาณคือ "วางตัวอย่างที่มีฟิลด์มากที่สุด แล้ว schema จะสมบูรณ์" การวัดกลับให้ผลตรงกันข้าม — และบุคคลที่สามได้ทำการทดลองควบคุมขนาดใหญ่กว่า

absurdtools ใช้ "ตระกูลข้อมูล" แบบสุ่ม 3000 ตระกูล ตระกูลละ 8 เรกคอร์ด โดยแต่ละฟิลด์ optional ปรากฏประมาณครึ่งหนึ่งของจำนวนครั้ง และวัดว่าข้อมูลจำนวนเท่าใดที่ schema ซึ่งสร้างจากตัวอย่างที่มีจำนวนคีย์ต่างกันสามารถยอมรับได้:

จำนวนคีย์ในตัวอย่าง สัดส่วนข้อมูลของตระกูลที่ถูกยอมรับ
2 100.0%
3 56.8%
4 34.8%
5 23.4%
6 17.7%
7 14.9%

ข้อสรุปไม่ใช่ "ลดลงเล็กน้อย" แต่เป็นการกลับทิศอย่างสมบูรณ์: ตัวอย่างที่น้อยที่สุดสร้าง schema ที่หลวมที่สุดใน 95% ของตระกูล ขณะที่ตัวอย่างที่สมบูรณ์ที่สุดทำได้เพียง 0.1% ของเวลา

เหตุผลอยู่ที่ความหมายของ required ซึ่งหมายถึง "ทั้งหมด"; ความเป็น optional สังเกตได้จากการหายไปเท่านั้น การวางเรกคอร์ดที่มีทุกฟิลด์บอก generator ว่า "ไม่มีคีย์ใดในนี้ที่ละเว้นได้"; การวางเรกคอร์ดที่ขาดฟิลด์มอบหลักฐานให้มันว่า "คีย์นี้อาจไม่มีก็ได้"

จำไว้หนึ่งประโยค: คุณค่าของตัวอย่างไม่ใช่ความสมบูรณ์ แต่คือการหายไปใดที่มันครอบคลุม

สุ่มกี่เรกคอร์ดจึงพอ

การทดลองชุดเดียวกันยังวัดความเร็วในการลู่เข้า: การสะสมเรกคอร์ดทีละตัวภายใต้กฎ "คีย์ยังคง required ก็ต่อเมื่อทุกเรกคอร์ดที่ผ่านมามีคีย์นั้น" จำนวนเรกคอร์ดมัธยฐานที่ทำให้ required ลู่เข้าเป็นชุด required จริงคือ 3; 6 เรกคอร์ดครอบคลุมประมาณเก้าสิบเปอร์เซ็นต์ของตระกูล และ 12 ครอบคลุมทั้งหมด

ดังนั้นการสุ่ม 5–10 เรกคอร์ดในชีวิตประจำวันให้ผลคุ้มแรงที่สุด; การบังคับให้ถึงยี่สิบกว่าตัวให้ผลเพิ่มขึ้นน้อยมาก

แต่มีเงื่อนไขเบื้องต้น: เรกคอร์ดเหล่านี้ต้องมาจากสถานการณ์ที่แตกต่างกันจริง ๆ ห้าเรกคอร์ดที่คัดลอกจากเทมเพลตเดียวกันจะหยุดให้ข้อมูลใหม่ตั้งแต่ตัวที่สองเป็นต้นไป — ชุดฟิลด์เหมือนกันทุกประการ และ required ไม่สามารถแคบลงได้แม้แต่คีย์เดียว

4. ทางแก้: ป้อนหลายเรกคอร์ดพร้อมกันและปล่อยให้การหายไปพูดแทน

เนื่องจากการสุ่มเรกคอร์ดเดียวเข้มงวดเกินไปเสมอ วิธีที่ถูกต้องคือให้ generator ดูหลายเรกคอร์ดพร้อมกันและลู่เข้า required ด้วยกฎ "required ก็ต่อเมื่อทุกเรกคอร์ดมีคีย์นั้น"

generator บนเว็บไซต์นี้ รับอาร์เรย์โดยตรงและมีสวิตช์ "all required" ปิดมันแล้วมันจะตัดสินจากจำนวนครั้งที่ปรากฏ: เฉพาะคีย์ที่ทุกเรกคอร์ดมีจะถูกเขียนลง required

ยังเป็นห้าเรกคอร์ดเดิม — ครั้งนี้รวมเป็นอาร์เรย์และอนุมานพร้อมกัน แล้วใช้ items เป็นสัญญาของสมาชิก:

เรกคอร์ดที่ป้อน required ที่อนุมานได้ การตรวจสอบทั้ง 5
1 เรกคอร์ด id, name, email, tags, score ผ่าน 3
2 เรกคอร์ด id, name, tags, score ผ่าน 4
3 เรกคอร์ด id, name, tags, score ผ่าน 4
4 เรกคอร์ด id, name, tags, score ผ่าน 5
5 เรกคอร์ด id, name, tags, score ผ่าน 5

required ลู่เข้าทันทีที่เพิ่มเรกคอร์ดที่สี่ และไม่เปลี่ยนอีกหลังจากนั้น email และ admin ถูกวางใน properties อย่างถูกต้องแต่ไม่อยู่ใน required และเนื่องจาก email ของเรกคอร์ดที่ 4 เป็น null การอนุมานหลายเรกคอร์ดจึงรวมมันเป็น {"type": ["string", "null"]} ทำให้ทั้งสตริงและ null ผ่าน — สิ่งที่การสุ่มเรกคอร์ดเดียวทำไม่ได้เลย

นี่อยู่ในระดับเดียวกับข้อค้นพบของ absurdtools ที่ว่า "required ลู่เข้าหลังเรกคอร์ดมัธยฐาน 3 ตัว"

อย่างไรก็ตาม การอนุมานหลายเรกคอร์ดมีจุดบอดของตัวเอง: ถ้า email ในเรกคอร์ดเหล่านี้บังเอิญเป็น null ทุกตัว (เช่นล้วนมาจากบัญชีที่ยังไม่กรอกโปรไฟล์) ผลการอนุมานจะกลายเป็น {"type": "null"} และ email ที่เป็นสตริงยังคงถูกปฏิเสธ เช่นเดียวกัน ถ้าค่า tags ทุกตัวเป็น array ว่าง จะอนุมานชนิดของสมาชิกไม่ได้เลย

ดังนั้นการสุ่มมีข้อกำหนดเพิ่มอีกข้อ: ทุกรูปแบบของทุกฟิลด์ต้องปรากฏอย่างน้อยหนึ่งครั้ง นำมาทั้งสตริง ตัวเลข null และ array ว่าง; อย่าให้มีรูปแบบใดหายไปทั้งหมด

เป็นรูปธรรม:

  1. ดึงการตอบกลับ 5–20 รายการจาก log, mock หรือคำขอจริง และต้องรวมแถวที่ผิดปกติ (แถวที่ฟิลด์ว่าง แถวที่มี null แถวที่มี array ว่าง);
  2. ประกอบเป็นอาร์เรย์ JSON แล้ววางลงใน generator;
  3. ปิด "all required" และเลือก draft 2020-12;
  4. ผลลัพธ์คือ type: array พร้อม items; ใช้ส่วน items เป็นสัญญาของเรกคอร์ดเดียว;
  5. ตรวจสอบตัวอย่างต้นฉบับด้วย schema นี้ทันที — ต้องผ่าน 100% ถ้าแม้แต่ตัวเดียวล้มเหลว การสุ่มของคุณยังไม่พอ; กลับไปเพิ่มเรกคอร์ด

ขั้นที่ 5 คือการตรวจสอบตนเองที่ถูกที่สุด ถ้า generator รองรับข้อมูลที่คุณป้อนให้มันไม่ได้เลย วิธีการสุ่มของคุณผิด และไม่มีประโยชน์ที่จะไปต่อ

5. สี่สิ่งที่ generator เติมให้ไม่ได้ — คุณต้องเขียนเอง

แม้หลังป้อนหลายเรกคอร์ด สิ่งต่อไปนี้ยังอนุมานไม่ได้และต้องเพิ่มด้วยมือ:

ส่วนที่ขาด พฤติกรรมที่วัดได้ ผลที่ตามมา วิธีแก้
รูปแบบสตริง "a@b.com" ให้เพียง {"type": "string"} การตรวจ "not an email" ผ่าน เพิ่ม "format": "email" และเปิด format assertion
ช่วงและ enum มองไม่เห็นว่า score จำกัด 0–100 หรือ status มีเพียงสามค่า จำนวนลบ ค่ามหาศาล และค่า enum ขยะล้วนผ่าน เพิ่ม minimum / maximum / enum
นโยบายฟิลด์ที่ไม่รู้จัก additionalProperties ไม่ถูกสร้าง คีย์ที่สะกดผิดถูกรับอย่างเงียบ ๆ ซึ่งอันตรายกว่า error เพิ่ม "additionalProperties": false สำหรับไฟล์แบบ config
ความหมายของ nullability และคอลเลกชันว่าง ตัวอย่างเดียวที่มี null ถูกตรึงเป็น {"type": "null"}; array ว่างให้ "items": {} ค่าที่ถูกต้องถูกปฏิเสธ หรือสมาชิก array ไม่ถูกจำกัดเลย เปลี่ยนเป็น ["string", "null"] และให้ array ว่างมีสมาชิกจริง

แถวที่สามควรกล่าวถึงแยก: ค่าเริ่มต้นของ additionalProperties คือ "อนุญาต" นั่นหมายถึงไฟล์ config ที่สะกด timeout เป็น timeuot จะผ่านการตรวจสอบแล้วไม่ทำงานใด ๆ ตอนรันไทม์อย่างเงียบ ๆ สำหรับ schema แบบ config นี่คือรายการที่คุณต้องปิดอย่างชัดเจน

format ก็มีกับดัก: ตั้งแต่ draft 2019-09 format อยู่ในคำศัพท์ annotation และโดยค่าเริ่มต้นมันเพียงบันทึกคำอธิบายไม่บังคับใช้ ถ้าคุณเพิ่ม format: "email" แล้วค่ายังผ่าน ในแปดจากสิบกรณี validator ยังไม่ได้เปิด validateFormats หรือปลั๊กอิน format ที่เกี่ยวข้อง — ไม่ใช่ schema ผิด

หมายเหตุเรื่องการเลือก draft ตรงนี้ด้วย: ถ้า schema จะไปอยู่ใน OpenAPI 3.1 ต้องเป็น draft 2020-12 เพราะ 3.1 ฝังเวอร์ชันนั้นโดยตรง; toolchain เก่า (ระบบนิเวศ Swagger 2.0 และ implementation $ref บางตัว) ยังเน้น draft-07 ทั้งสองมีพฤติกรรมต่างกันสำหรับคีย์เวิร์ดข้าง $ref และคีย์เวิร์ดเกี่ยวกับ array ดังนั้นอย่าผสมกันในชุด schema เดียว

6. สำหรับ API ของบุคคลที่สามที่ไม่มีเอกสาร ให้วิศวกรรมย้อนกลับสัญญาจากตัวอย่างการตอบกลับ

การเชื่อม callback ของบุคคลที่สาม การทำงานกับพาร์ทเนอร์ที่ไม่ออกเอกสาร หรือการดูแลระบบเก่าอายุสิบปี — ทั้งหมดนี้หมายถึง "ข้อมูลอยู่ในมือ แต่สเปกไม่อยู่" ที่นี่ generator ไม่ใช่เพียงความสะดวก; มันคือเส้นทางที่เร็วที่สุดสู่สัญญา

วิธีการเหมือนข้างต้น แต่มีข้อบังคับเพิ่มสองข้อ:

หนึ่ง: สุ่มข้ามเวลาและข้ามสถานะ ฟิลด์ของบุคคลที่สามมักเปลี่ยนตามสถานะธุรกิจ — โปรโมชันเพิ่ม discount คำสั่งคืนเงินเพิ่ม refundAt และการตอบกลับที่ผิดพลาดไม่มี data เลย การดึงสแนปช็อตครั้งเดียวหมายถึงเห็นเพียงสถานะเดียว

ป้อนความสำเร็จหลายรายการ ความล้มเหลวหลายรายการ และผลลัพธ์ว่างหลายรายการรวมกัน: error กับ data มักปรากฏแบบแยกกัน การอนุมานหลายเรกคอร์ดจึงวางทั้งสองใน properties แต่ไม่ใส่ใน required — ตรงกับความเป็นจริงพอดี

สอง: เก็บ schema ไว้ใน repository และแนบการตรวจประจำวัน ตรวจสอบการตอบกลับจริงของวันนี้เทียบกับมัน แล้วคุณจะรู้ว่าฟิลด์ใดหายไปเงียบ ๆ หรือเปลี่ยนชนิดก่อนผู้ใช้ของคุณ

// รันวันละครั้ง: แจ้งเตือน contract drift
const drifted = records.filter((record) => !validate(record));

if (drifted.length > 0) {
  console.warn(
    `${drifted.length} responses do not match the contract; first difference:`,
    validate.errors,
  );
}

คุณค่าของแนวปฏิบัตินี้ไม่ใช่ "ประหยัดแรงในการเขียนด้วยมือ" แต่คือการเปลี่ยนข้อตกลงด้วยวาจาให้เป็นการแจ้งเตือนที่รันได้ บุคคลที่สามไม่เคยบอกล่วงหน้าว่าเปลี่ยนฟิลด์ แต่การเปลี่ยนแปลงจะปรากฏใน log ของคุณแน่นอน

ขอบเขตหนึ่งที่ต้องขีดให้ชัด: schema ที่วิศวกรรมย้อนกลับอธิบายสิ่งที่อีกฝ่ายส่งกลับ ตอนนี้ ไม่ใช่สิ่งที่มัน สัญญาว่าจะส่งกลับ แบบแรกใช้สำหรับการมอนิเตอร์; มีเพียงแบบหลังเท่านั้นที่ควรอยู่ในสัญญาหรือเอกสาร API

7. หลังทำให้เสร็จ ให้ error ของการตรวจสอบชี้กลับไปยังฟิลด์ที่เฉพาะเจาะจง

เมื่อ schema ใช้งานได้แล้ว ความกังวลถัดไปคือ error ที่อ่านง่าย ออบเจกต์ error ของ ajv มีห้าฟิลด์ และสี่ฟิลด์แรกคือฟิลด์ที่มีประโยชน์:

ฟิลด์ ความหมาย ตัวอย่าง (id: 0 ละเมิด minimum: 1)
instancePath JSON Pointer ไปยังค่าที่ผิดในข้อมูล "/id"
schemaPath ตำแหน่งของกฎที่ทริกเกอร์ใน schema "#/properties/id/minimum"
keyword ชื่อของข้อจำกัดที่ล้มเหลว "minimum"
params รายละเอียดที่เครื่องอ่านได้ของข้อจำกัดนั้น { "comparison": ">=", "limit": 1 }
message ข้อความภาษาอังกฤษที่เปลี่ยนไปตามเวอร์ชัน อย่า parse "must be >= 1"

ความไม่สมมาตรหนึ่งที่พลาดง่ายที่สุด: เมื่อ required ล้มเหลว instancePath ชี้ไปที่ออบเจกต์แม่ (โดยปกติสตริงว่าง คือราก) ไม่ใช่ฟิลด์ที่หายไป ชื่อของคีย์ที่หายไปอยู่ใน params.missingProperty เช่นเดียวกัน เมื่อ additionalProperties ล้มเหลว ชื่อคีย์เกินอยู่ใน params.additionalProperty

ดังนั้นเมื่อแมป error เป็น "ฟิลด์ → ข้อความ" ให้จัดการสองกรณีพิเศษนี้ก่อน:

import Ajv from "ajv";

const ajv = new Ajv({ allErrors: true });
const validate = ajv.compile(schema);

/** แปลงอาร์เรย์ error ของ ajv เป็น { fieldPath: [message] } สำหรับไฮไลต์ฟอร์ม */
function groupByField(errors) {
  const grouped = {};

  for (const error of errors) {
    // สำหรับ required และ additionalProperties ฟิลด์ที่ผิดอยู่ใน params ไม่ใช่ instancePath
    const field =
      error.keyword === "required"
        ? error.params.missingProperty
        : error.keyword === "additionalProperties"
          ? error.params.additionalProperty
          : error.instancePath.replace(/^\//u, "").split("/").join(".") ||
            "(root)";

    (grouped[field] ??= []).push(error.message);
  }

  return grouped;
}

if (!validate(data)) console.log(groupByField(validate.errors));

allErrors: true ไม่ใช่ตัวเลือก ภายใต้การตั้งค่าเริ่มต้น ajv หยุดที่ error แรก ผู้ใช้จึงแก้ฟิลด์หนึ่ง ส่งใหม่ ได้ error ถัดไป และต้องไปกลับสามรอบเพียงเพื่อกรอกฟอร์มเดียว ถ้าคุณใช้ ajv-errors สำหรับข้อความกำหนดเองด้วย หากไม่มีธงนี้ ปลั๊กอินจะแทนที่ข้อความเพียงบางส่วนอย่างเงียบ ๆ

8. กระบวนการนำไปใช้ที่ใช้ซ้ำได้

ร้อยเรียงขั้นตอนข้างต้นเป็นกิจวัตรตายตัวและทำตามทุกครั้งที่เชื่อม API ใหม่:

  1. สุ่ม: ดึงการตอบกลับจริง 5–20 รายการ โดยเลือกตัวที่มีฟิลด์ว่าง null array ว่าง และฟิลด์เกินอย่างตั้งใจ;
  2. อนุมาน: วางรวมกันเป็นอาร์เรย์ลงใน generator ปิด "all required" แล้วใช้ items;
  3. เติมความหมาย: เพิ่ม format, enum, minimum/maximum, minItems และ additionalProperties ทีละรายการ;
  4. ตรวจสอบตนเอง: ตัวอย่างต้นฉบับต้องผ่าน 100% มิฉะนั้นกลับไปขั้นที่ 1 และเพิ่มตัวอย่าง;
  5. พิสูจน์เชิงลบ: เตรียมอินพุต "ควรล้มเหลว" สำหรับทุกข้อจำกัดและยืนยันว่ามันล้มเหลวจริง — ทดสอบเฉพาะเส้นทางที่ผ่านเท่ากับไม่ได้ทดสอบ;
  6. คอมมิต: เก็บ schema ไว้ใน version control เก็บตัวอย่างที่ล้มเหลวเป็นกรณีทดสอบ และรันการทดสอบก่อนทุกครั้งเมื่อ API เปลี่ยนฟิลด์

การทดสอบเชิงลบในขั้นที่ 5 ถูกข้ามง่ายที่สุด การเพิ่มคีย์ลง required โดยไม่เคยยืนยันว่าคีย์ที่หายไปก่อ error เท่ากับเดิมพันว่า validator เห็นตรงกับแบบจำลองในใจของคุณ

import { describe, expect, it } from "vitest";

describe("user schema", () => {
  it("accepts a complete record", () => {
    expect(validate({ id: 1, name: "Ada", tags: [], score: 10 })).toBe(true);
  });

  it("rejects a record without name", () => {
    expect(validate({ id: 2, tags: [], score: 7 })).toBe(false);
    // ยืนยันเหตุผลด้วย เพื่อไม่ให้การทดสอบผ่านเพราะฟิลด์อื่นล้มเหลว
    expect(
      validate.errors?.some((e) => e.params?.missingProperty === "name"),
    ).toBe(true);
  });
});

การยืนยันตัวสุดท้ายสำคัญ การยืนยันเพียง false อาจยังเขียวอยู่แม้ schema พังไปแล้ว — ตอนนั้นความล้มเหลวอาจมาจากสาเหตุที่ต่างกันโดยสิ้นเชิง

9. คำถามที่พบบ่อย

schema ที่ generator สร้างส่งขึ้นโปรดักชันได้เลยไหม

ไม่ได้ มันเป็นเพียงสแนปช็อตของโครงสร้าง คุณต้องเพิ่มรูปแบบ ช่วง enum และนโยบายฟิลด์ที่ไม่รู้จักอย่างน้อย แล้วรันทั้งตัวอย่างต้นฉบับและตัวอย่างเชิงลบผ่านมัน generator แก้ปัญหา "พิมพ์ properties ด้วยมือมันน่าเบื่อ" ไม่ใช่ "มันตัดสินกฎธุรกิจ"

สำหรับระบบคีย์เวิร์ดของการเขียน schema การแยกด้วย $ref และคีย์เวิร์ดการประกอบ ดู JSON Schema: จากพื้นฐานสู่การใช้งานจริง

ทำไมเรกคอร์ดที่มีฟิลด์มากที่สุดกลับปฏิเสธข้อมูลมากกว่า

เพราะ required ถูกรวบรวมจาก "สิ่งที่ปรากฏในตัวอย่าง" ตัวอย่างที่ครบทุกฟิลด์เปลี่ยนทุกคีย์ optional ให้เป็นคีย์บังคับ ขณะที่คีย์เหล่านั้นส่วนใหญ่หายไปในข้อมูลจริง การป้อนหลายเรกคอร์ด หรือเรกคอร์ดที่ ละเว้นฟิลด์อย่างตั้งใจ ทำให้คุณเข้าใกล้ความจริงมากกว่าตัวอย่างที่สมบูรณ์ที่สุด

จำนวนเต็มจะถูกอนุมานเป็น integer ไหม

generator บนเว็บไซต์นี้ไม่ทำเช่นนั้น: ทั้งจำนวนเต็มและทศนิยมถูกส่งออกเป็น number ดังนั้น schema ที่สร้างจาก {"price": 5} จะยอมรับ 5.99 สิ่งนี้หลีกเลี่ยงอุบัติเหตุอย่าง "ตัวอย่างบังเอิญเป็นจำนวนเต็ม โปรดักชันจึงปฏิเสธราคาที่มีทศนิยม" เครื่องมือของบุคคลที่สามบางตัวอนุมาน integer จากค่าจริง ดังนั้นระวังฟิลด์เงินเป็นพิเศษ

สำหรับรายละเอียดความแม่นยำของตัวเลขและขอบเขตของชนิด ดู หกชนิดข้อมูล JSON และกับดักความแม่นยำของตัวเลข

เพิ่ม format แล้วแต่ไม่เกิดผล

ตั้งแต่ draft 2019-09 format เป็น annotation-only โดยค่าเริ่มต้น ให้เปิด format assertion ใน validator อย่างชัดเจน (ajv ต้องใช้ ajv-formats) หรือใช้ pattern เป็นข้อจำกัดแบบเข้มงวด อย่าสมมติว่าการเพิ่มมันจะบล็อกอะไร

required รายงาน error แต่ฟิลด์ปรากฏชัด ๆ ในข้อมูล

ดู instancePath ก่อน error ของ required ชี้ไปที่ออบเจกต์แม่ ซึ่งหมายความว่าคีย์ที่หายไปไม่ได้อยู่ระดับนั้น — สาเหตุที่พบบ่อยคือระดับการซ้อนผิด (/profile/name ไม่เหมือน name ที่ราก) ตัวพิมพ์ต่างกัน (userId เทียบกับ userID) หรือการ serialize ต้นทางทิ้ง undefined ไปทั้งหมด

ก่อนสอบสวน ให้ยืนยันว่าเอกสารเองเป็น JSON ที่ถูกต้อง: comment, trailing comma และปัญหาคล้ายกันล้มเหลวตั้งแต่ขั้น parse ก่อนที่ schema จะมีโอกาสทำงาน JSON Parse & Format ชี้ไปที่บรรทัดที่ผิดโดยตรง

ควรสุ่มกี่เรกคอร์ด และควรใช้ draft ไหน

การสุ่ม 5–10 เรกคอร์ดในชีวิตประจำวันให้ผลคุ้มที่สุด; required ลู่เข้าที่มัธยฐาน 3 เรกคอร์ด และ 12 เรกคอร์ดครอบคลุมทุกตระกูล แต่ละเรกคอร์ดต้องมาจากสถานการณ์ที่แตกต่างกันจริง ๆ และทุกรูปแบบของทุกฟิลด์ต้องปรากฏอย่างน้อยหนึ่งครั้ง ตรึง draft ไว้ที่ 2020-12 เพราะ OpenAPI 3.1 ยอมรับเฉพาะเวอร์ชันนั้น

ทำไมค่าเริ่มต้น additionalProperties ที่ผ่อนปรนจึงอันตรายกว่า

ค่าเริ่มต้นไม่ปล่อยคีย์เวิร์ดใดเลย คีย์ที่สะกดผิดจึงถูกรับอย่างเงียบ ๆ ผ่านการตรวจสอบ แล้วล้มเหลวตอนรันไทม์ — วินิจฉัยยากกว่า error โดยตรง schema แบบ config ต้องตั้ง "additionalProperties": false อย่างชัดเจน ขณะที่ schema ของ API ควรชั่งน้ำหนักต้นทุนด้านความเข้ากันได้ก่อน

ควรใช้ schema ที่วิศวกรรมย้อนกลับเป็นด่านเข้มงวดไหม

ไม่ได้ schema ที่วิศวกรรมย้อนกลับอธิบายสิ่งที่อีกฝ่ายส่งกลับ ตอนนี้ ไม่ใช่สิ่งที่มัน สัญญาว่าจะส่งกลับ จึงเหมาะกับการมอนิเตอร์และการแจ้งเตือน drift สิ่งใดที่จะอยู่ในสัญญาหรือเอกสาร API ต้องเป็นสัญญาที่ให้คำมั่น