JSON Toolkit

เมื่อ error ของ JSON Schema ไม่มีความหมาย: การระบุตำแหน่งและ governance ข้อความ error ของ Ajv

สรุปสำคัญ: validator ปล่อย error ที่เครื่องอ่านได้; ผู้ใช้ต้องการรู้ว่าช่องกรอกไหนผิด ครอบคลุมอาร์เรย์ errors ของ Ajv, instancePath เทียบกับ schemaPath, การจัดกลุ่มและถ้อยคำ

ขั้นสูงอ่านประมาณ 10 นาทีเผยแพร่ 2026-10-31อัปเดต 2026-10-31
แผนภาพการระบุตำแหน่ง error ของ Ajv และ governance ข้อความ

สรุปสั้น: อ่าน instancePath ก่อนเพื่อระบุข้อมูลที่ล้มเหลว จากนั้นอ่าน schemaPath เพื่อระบุกฎที่ถูกทริกเกอร์; สำหรับ required และ additionalProperties ให้ดึงชื่อคีย์จริงจาก params ก่อนเลือกข้อความ

หน้าที่ของ validator คือบอกว่ากฎใดล้มเหลว การแปลสิ่งนั้นเป็น "ช่องกรอกนี้ผิด" เป็นหน้าที่ของคุณ

1. อาร์เรย์ errors ให้อะไรคุณจริง ๆ

Ajv ไม่ throw เมื่อการตรวจสอบล้มเหลว; มันเก็บผลลัพธ์ไว้ใน validate.errors การตั้งค่าเริ่มต้นเก็บเพียง error แรก และ allErrors คือสิ่งที่สร้างรายการเต็ม ออบเจกต์ error หนึ่งตัวมีห้าฟิลด์ที่สำคัญ และการเข้าใจมันเป็นเงื่อนไขเบื้องต้นของทุกอย่างที่ตามมา:

ฟิลด์ ความหมาย ตัวอย่าง
instancePath JSON Pointer ไปยังค่าที่ล้มเหลว /email
schemaPath ตำแหน่งของคีย์เวิร์ดที่ล้มเหลวใน schema #/properties/email/format
keyword ชื่อของข้อจำกัดที่ถูกละเมิด format
params พารามิเตอร์ที่เครื่องอ่านได้ของข้อจำกัดนั้น { "format": "email" }
message ข้อความภาษาอังกฤษที่เปลี่ยนไปตามเวอร์ชัน must match format "email"

error จริงมีหน้าตาดังนี้:

[
  {
    "instancePath": "/email",
    "schemaPath": "#/properties/email/format",
    "keyword": "format",
    "params": { "format": "email" },
    "message": "must match format \"email\""
  }
]

โปรดสังเกตว่า message เป็นถ้อยคำที่เขียนเพื่อมนุษย์ ไม่ใช่อินเทอร์เฟซที่เสถียร Ajv อาจเปลี่ยนถ้อยคำระหว่างเวอร์ชัน และมันไม่สามารถรองรับผลิตภัณฑ์ที่แปลหลายภาษาได้ อย่า parse มันด้วย regular expression และอย่าคืนมันตามต้นฉบับให้ไคลเอนต์

2. instancePath กับ schemaPath ตอบคำถามต่างกัน

instancePath บอกว่าข้อมูลส่วนใดล้มเหลว; schemaPath บอกว่ากฎใดถูกทริกเกอร์ ทั้งสองเป็นระบบพิกัดคนละชุด และไม่สมมาตรกัน:

  • instancePath เป็น JSON Pointer ตาม RFC 6901 สตริงว่างหมายถึงราก; /items/0/price หมายถึงฟิลด์ price ของสมาชิก array ตัวแรก
  • schemaPath เริ่มต้นด้วย # และชี้ไปยังคีย์เวิร์ดภายในเอกสาร schema เมื่อข้าม $ref มันจะชี้ภายใน schema ที่ถูกอ้างอิง คุณจึงต้องมี $id เพื่อรู้ว่ามันมาจากไฟล์ใด
  • คีย์ที่มี / หรือ ~ จะถูก escape เป็น ~1 และ ~0 ตามลำดับ ให้ย้อนกลับก่อนแสดงผล มิฉะนั้น UI จะแสดงสัญลักษณ์ที่ชวนงง

ตัวอย่างเช่น instancePath: "/email" พร้อม schemaPath: "#/properties/email/format" หมายถึงฟิลด์ email บนออบเจกต์รากไม่ผ่านการตรวจรูปแบบ

กับดักที่พบบ่อยที่สุดคือ required และ additionalProperties: instancePath ของมันชี้ไปที่ออบเจกต์แม่ ไม่ใช่คีย์ที่ผิด

คีย์เวิร์ด instancePath ชี้ไปที่ คีย์จริงอยู่ที่ไหน
required ออบเจกต์แม่ (สตริงว่างที่ราก) params.missingProperty
additionalProperties ออบเจกต์แม่ params.additionalProperty
type ค่าที่ล้มเหลวเอง —
enum ค่าที่ล้มเหลวเอง params.allowedValues

เมื่อแมป error ลงบนฟิลด์ฟอร์ม ให้จัดการสองกรณีนี้ก่อน มิฉะนั้น "email หายไป" จะกลายเป็น "error ที่ออบเจกต์ราก" และผู้ใช้ไม่รู้ว่าจะพิมพ์ที่ไหน

3. allErrors กับ verbose: ความครบถ้วนเทียบกับต้นทุน

ค่าเริ่มต้น allErrors: false ทำให้ Ajv หยุดที่ความล้มเหลวแรก ซึ่งดีสำหรับเครื่องมือบรรทัดคำสั่งแต่หายนะสำหรับฟอร์ม: ผู้ใช้แก้ฟิลด์หนึ่ง ส่งฟอร์ม เห็น error ถัดไป และทำซ้ำสามรอบกว่าจะผ่าน

การตั้งค่า พฤติกรรม ประโยชน์ ต้นทุน
allErrors: false เฉพาะ error แรก เร็วที่สุด หน่วยความจำน้อยที่สุด ต้องส่งซ้ำหลายรอบ
allErrors: true เก็บทุก error เห็นทุกปัญหาในครั้งเดียว จำนวน error พองเมื่อ schema ซับซ้อน
verbose: true แนบ schema และ data กับทุก error วินิจฉัยฝั่งเซิร์ฟเวอร์ง่ายขึ้น ขนาดเพิ่มขึ้นมาก; ห้ามเปิดเผย
messages: false ตัดฟิลด์ message ออก payload เล็กลง เสียเบาะแสการดีบักภาษาอังกฤษ

คำแนะนำในการแยกใช้: เปิด allErrors: true สำหรับการตรวจสอบฝั่งเซิร์ฟเวอร์และเก็บอาร์เรย์เต็มใน log ภายใน ขณะที่การตอบกลับสาธารณะบรรจุเฉพาะฟิลด์ที่อนุญาต เปิด verbose เฉพาะเมื่อสอบสวน เพราะมันฝัง fragment ของ schema ที่ตรงและข้อมูลของผู้ใช้ลงในทุก error ซึ่งกินหน่วยความจำและอาจรั่วค่าที่ละเอียดอ่อนลง log

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

4. จัดกลุ่ม error ตามฟิลด์

การจัดกลุ่มทำสามสิ่ง: เปลี่ยนพาธกลับเป็นชื่อฟิลด์ แก้จุดอ้างอิงของ required และ additionalProperties และรวม error หลายตัวบนฟิลด์เดียวกันเป็นข้อความที่ลงมือได้ที่สุด

/** error ของการตรวจสอบหนึ่งรายการที่พร้อมแสดง */
interface FieldError {
  field: string;
  keyword: string;
  params: Record<string, unknown>;
}

/**
 * จัดกลุ่ม error ของ Ajv ตามฟิลด์
 * @param errors อาร์เรย์ error ที่ Ajv สร้าง
 * @returns Map ที่คีย์เป็นพาธฟิลด์ แต่ละค่าเป็นรายการ error ของฟิลด์นั้น
 */
function groupByField(errors: Array<Record<string, any>>): Record<string, FieldError[]> {
  const grouped: Record<string, FieldError[]> = {};

  for (const errorItem of errors) {
    const keyword: string = errorItem.keyword;
    let field: string = errorItem.instancePath;

    if (keyword === 'required') {
      const parentPath: string = errorItem.instancePath;
      field = `${parentPath}/${errorItem.params.missingProperty}`;
    } else if (keyword === 'additionalProperties') {
      field = `${errorItem.instancePath}/${errorItem.params.additionalProperty}`;
    }

    const normalizedField = field === '' ? '(root)' : field;
    (grouped[normalizedField] ??= []).push({
      field: normalizedField,
      keyword,
      params: errorItem.params ?? {},
    });
  }

  return grouped;
}

รวมด้วยลำดับความสำคัญ: required (หายไป) ก่อน จากนั้น type จากนั้น enum/const จากนั้น format/pattern จากนั้นความยาวและช่วง แสดงเพียงข้อความที่สำคัญที่สุดต่อฟิลด์และเก็บที่เหลือไว้ใน log เหตุผลตรงไปตรงมา: เมื่อชนิดผิด ข้อความเรื่องความยาวคือเสียงรบกวน และการแก้ชนิดคือก้าวถัดไปที่สมเหตุสมผลเพียงอย่างเดียว

error สรุปของ oneOf และ anyOf มักไม่มีข้อมูลที่ลงมือได้ ให้ข้ามไปตอนเรนเดอร์และเก็บเฉพาะ error ของสาขาที่ชี้ไปยังฟิลด์ที่เป็นรูปธรรม

5. ข้อความกำหนดเอง: เส้นทางปลั๊กอินและเส้นทางเทมเพลต

Ajv ไม่มีถ้อยคำที่แปลไว้ จึงมีสองทางเลือกในทางปฏิบัติ

ทางเลือกแรก: ปลั๊กอิน ajv-errors มันเพิ่มคีย์เวิร์ด errorMessage เพื่อให้ข้อความอยู่ข้างข้อจำกัด:

{
  "properties": {
    "email": {
      "type": "string",
      "format": "email",
      "errorMessage": {
        "format": "Please enter a valid email address",
        "type": "Email must be a string"
      }
    }
  }
}

ข้อดีคือถ้อยคำไปกับฟิลด์และ schema ทำหน้าที่เป็นเอกสารไปในตัว ข้อเสียคือข้อความถูกตรึงไว้ในภาษาเดียว การแปลจึงหมายถึงต้องดูแลหลาย schema ปลั๊กอินยังแทนที่ทุกข้อความได้เฉพาะเมื่อตั้ง allErrors: true; ลืมธงนี้แล้วคุณจะได้ข้อความกำหนดเองปนกับภาษาอังกฤษเริ่มต้น

ทางเลือกที่สอง: ตาราง keyword-to-template validator ให้เพียง keyword และ params; แอปพลิเคชันเป็นผู้สร้างถ้อยคำ:

คีย์เวิร์ด เทมเพลต พารามิเตอร์ที่ใช้ได้
required ฟิลด์ที่จำเป็นหายไป: {field} missingProperty
type ชนิดที่คาดหวัง: {type} type
minLength กรอกอย่างน้อย {limit} อักขระ limit
maximum ต้องไม่เกิน {limit} limit
format รูปแบบไม่ถูกต้อง: {format} format
enum ต้องเป็นหนึ่งในตัวเลือกที่อนุญาต allowedValues
pattern ค่าไม่ตรงกับรูปแบบที่คาดหวัง pattern (อย่าสะท้อนกลับ)

ทางเลือกที่สองเหมาะกับผลิตภัณฑ์ที่ต้องใช้หลายภาษาและน้ำเสียงที่สม่ำเสมอ มันแยกการตรวจสอบออกจากการนำเสนอ เก็บการแปลไว้นอก schema และให้ frontend ใช้เทมเพลตเดียวกันซ้ำได้

6. การแปลภาษา: ข้อความไม่ใช่ของ validator

การย้ายถ้อยคำไปยัง language pack คือจุดเริ่มต้นของ governance ข้อความ language pack เก็บเทมเพลตและคีย์ ไม่เก็บชื่อฟิลด์; ป้ายชื่อฟิลด์มาจากการตั้งค่าฟอร์มหรือสัญญา API และทั้งสองถูกรวมกันตอนเรนเดอร์

/** เทมเพลตข้อความตรวจสอบที่คีย์ตามคีย์เวิร์ดของ Ajv */
const VALIDATION_TEMPLATES: Record<string, (params: Record<string, any>) => string> = {
  required: (params) => `Missing required field: ${params.missingProperty}`,
  type: (params) => `Expected type: ${params.type}`,
  minLength: (params) => `Enter at least ${params.limit} characters`,
  maximum: (params) => `Must not exceed ${params.limit}`,
  format: () => 'Invalid format',
};

/**
 * สร้างข้อความตรวจสอบหนึ่งข้อความสำหรับผู้ใช้
 * @param keyword ชื่อของข้อจำกัดที่ล้มเหลว
 * @param params พารามิเตอร์ของข้อจำกัด
 * @returns ข้อความที่แปลแล้ว
 */
function buildMessage(keyword: string, params: Record<string, any>): string {
  const template = VALIDATION_TEMPLATES[keyword];
  return template ? template(params) : 'The value is invalid';
}

กฎสามข้อสำคัญ ณ จุดนี้:

  1. อย่าต่อประโยคด้วยสตริง "Email format is invalid" กับคู่แปลภาษาไทยไม่ได้มีลำดับคำเดียวกัน การประกอบสตริงสุดท้ายจะสร้างประโยคไร้ความหมายในภาษาใดภาษาหนึ่ง
  2. มอบพหูพจน์และลักษณนามให้ไลบรารี i18n อย่า hardcode "characters" ในเทมเพลต; 1 character กับ 3 characters ต่างกัน
  3. แปลป้ายชื่อฟิลด์ด้วย params.missingProperty คือ email ขณะที่ UI ควรเขียน "อีเมล" ให้เก็บการแมปนั้นในการตั้งค่าฟอร์ม ไม่ใช่ใน language pack

7. อย่าคืนโครงสร้างภายในให้ไคลเอนต์

การคืน schemaPath, params และ message พร้อมกันสะดวกตอนพัฒนาแต่เป็นการรั่วข้อมูลในโปรดักชัน allowedValues เผย enum ภายใน pattern เผย regex สำหรับตรวจสอบ และ schemaPath เผยโครงสร้างไฟล์และรูปแบบการตั้งชื่อฟิลด์ — ในบางกรณีเท่ากับมอบแผนที่จุดอ่อนให้ผู้โจมตี

การตอบกลับสาธารณะเก็บเพียงสามอย่าง: พาธฟิลด์ (หรือชื่อฟิลด์ฟอร์ม) รหัส error ที่เสถียร และข้อความสำหรับผู้ใช้ ใช้รหัสสั้นที่เสถียรแทน keyword ดิบ เพื่อให้ยังมีที่ว่างสำหรับเปลี่ยน validator ในภายหลัง

{
  "type": "about:blank",
  "title": "Request validation failed",
  "status": 422,
  "errors": [
    { "field": "email", "code": "invalid_format", "message": "Please enter a valid email address" },
    { "field": "items[0].price", "code": "invalid_pattern", "message": "Amount must have exactly two decimals" }
  ]
}

log ฝั่งเซิร์ฟเวอร์ควรเก็บสิ่งที่ตรงกันข้าม: อาร์เรย์ error เต็ม, request id และเวอร์ชัน schema เพื่อให้การสอบสวนตอบได้ว่า "กฎใดบล็อกสิ่งนี้" หลีกเลี่ยงการทิ้ง body ของคำขอทั้งก้อนลง log; เก็บเฉพาะพาธและ keyword ที่ล้มเหลวแทน

8. การนำเสนอฝั่ง frontend: จากไฮไลต์ฟิลด์ถึง marker ในเอดิเตอร์

error สุดท้ายไปลงที่ UI และการนำเสนอขึ้นกับบริบท:

บริบท การนำเสนอ การกระทำสำคัญ
ฟอร์ม ข้อความสีแดงใต้ฟิลด์ โฟกัส error แรก แมปพาธเป็นชื่อฟิลด์ฟอร์ม; เก็บดัชนี array ไว้
นำเข้าตารางจำนวนมาก หมายเลขแถว ชื่อคอลัมน์ สรุป ส่งออกได้ แปลง /items/3/price เป็นแถวที่ 4 คอลัมน์ price
เอดิเตอร์ JSON marker แบบอินไลน์และแผงปัญหา ใช้ parser แมป JSON Pointer เป็นบรรทัดและคอลัมน์
เครื่องมือดีบัก API รายการ error แบบมีโครงสร้างที่เก็บ schemaPath หันหน้าให้นักพัฒนา จึงรับรายละเอียดได้มากกว่า

หมายเหตุการ implement สามข้อ:

  • แสดงเพียง error แรกต่อฟิลด์ และยุบที่เหลือเป็น "อีก 2 ปัญหา" เพื่อให้ฟอร์มไม่จมอยู่ในสีแดง
  • เก็บดัชนี array ไว้สำหรับชี้เป้า แต่ตัดออกจากป้ายชื่อ "แถวที่ 4, price" อ่านง่ายกว่า /items/3/price
  • สถานการณ์ในเอดิเตอร์ต้องมีการแมปตำแหน่ง การค้นหาสตริงธรรมดาไม่น่าเชื่อถือเมื่อชื่อฟิลด์ซ้ำ; parser ควรให้ offset ของแต่ละโหนด ซึ่งคุณแปลงเป็นบรรทัดและคอลัมน์ต่อ

ถ้า JSON มาจากผู้ใช้พิมพ์แทนฟอร์ม ให้ยืนยันว่าเอกสารถูกต้องตามไวยากรณ์ก่อน error ทางไวยากรณ์เกิดขึ้นตอน parse และไม่เคยไปถึงการตรวจสอบ schema JSON Parse & Format รายงานบรรทัดและคอลัมน์ที่แน่นอน ซึ่งเร็วกว่าการตามหา error ของ schema มาก สำหรับสาเหตุที่ schema เข้มงวดเกินไปเมื่ออนุมานจากตัวอย่าง ดู ทำไม JSON Schema ที่สร้างอัตโนมัติจึงปฏิเสธข้อมูลจริงซ้ำ ๆ; สำหรับการตั้งค่า validator และระบบคีย์เวิร์ด ดู JSON Schema: จากพื้นฐานสู่การใช้งานจริง

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

ทำไมได้ error เพียงตัวเดียว

allErrors มีค่าเป็น false โดยค่าเริ่มต้น Ajv จึงคืนค่าที่ความล้มเหลวแรก ให้เปิดมันอย่างชัดเจนสำหรับการตรวจสอบฟอร์ม; มิฉะนั้นผู้ใช้ต้องส่งซ้ำหลายรอบเพื่อค้นหาทุกปัญหา

ทำไมพาธบน error ของ required ว่างเปล่า

instancePath ของ required ชี้ไปที่ออบเจกต์แม่ ซึ่งเป็นสตริงว่างที่ราก; ชื่อคีย์ที่หายไปอยู่ใน params.missingProperty ต่อทั้งสองเข้าด้วยกันเพื่อให้ได้พาธเต็ม การแสดง instancePath เพียงอย่างเดียวให้ "error ที่ออบเจกต์ราก" ที่ไม่มีประโยชน์

แสดง message ให้ผู้ใช้โดยตรงได้ไหม

ไม่แนะนำ มันเป็นถ้อยคำภาษาอังกฤษที่เปลี่ยนไปตามเวอร์ชัน และไม่มีความสามารถแปลภาษา ให้สร้างข้อความจาก keyword และ params แทน และเก็บ message ไว้สำหรับ log

ทำไมข้อความ error กำหนดเองของฉันถูกละเลย

ตรวจก่อนว่าเปิด allErrors: true แล้วหรือไม่; ajv-errors แทนที่ทุกข้อความได้เฉพาะเมื่อมีธงนี้ จากนั้นยืนยันว่า errorMessage อยู่ระดับเดียวกับคีย์เวิร์ดที่ล้มเหลว — เยื้องผิดไปหนึ่งระดับก็จะไม่ถูกจดจำ

ฟิลด์ใดปลอดภัยที่จะคืนในโปรดักชัน

พาธฟิลด์ รหัส error ที่เสถียร และข้อความสำหรับผู้ใช้ เก็บ schemaPath, params และ message ไว้ใน log ฝั่งเซิร์ฟเวอร์: สิ่งนั้นรองรับการดีบักโดยไม่เผย enum, regex หรือโครงสร้างไฟล์ภายใน