JSON Toolkit

คำถามที่พบบ่อยเรื่องการค้นหาและการตรวจสอบ: JSONPath และ Schema

สรุปสำคัญ: การ query ด้วย path ควรใช้ JSONPath หรือ Pointer, จะเลือก Patch กับ Merge Patch อย่างไร, Schema กันอะไรได้บ้าง และควรเลือก draft เวอร์ชันไหน — อธิบายครบในรอบเดียว

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

สรุปสั้น ๆ: ใช้ JSONPath เพื่อหา node ตามเงื่อนไข ใช้ JSON Pointer หรือ JSON Patch เพื่อชี้และแก้ไขตำแหน่งที่แน่นอน และใช้ Merge Patch เพื่อรวม config; Schema ใหม่ให้ปักหมุดที่ 2020-12 และเปิด allErrors เพื่อให้ข้อผิดพลาดทั้งหมดถูกส่งกลับมาในรอบเดียว

การ query คือ "หามันให้เจอ" ส่วนการ validation คือ "ยืนยันว่ามันถูก" ทั้งสองมีมาตรฐานของตัวเอง อย่าเอามาปนกัน

1. การ query ด้วย path

ควรเลือก JSONPath หรือ JSON Pointer?

ขึ้นกับว่าคุณกำลัง "หาด้วยเงื่อนไข" หรือ "ดึงด้วยตำแหน่ง"

มิติ JSONPath JSON Pointer
มาตรฐาน RFC 9535 (2024) RFC 6901 (2013)
Expression $.users[?(@.age > 18)].id /users/0/id
wildcard และการไล่ซ้ำ รองรับ *, .. ไม่รองรับ
การกรองตามเงื่อนไข รองรับ ไม่รองรับ
ผลลัพธ์ ชุดของ node (อาจหลายตัว) ตำแหน่งเดียว
ความสามารถในการแสดงออก สูง แต่มีภาระจากความต่างของ implementation ในอดีตมาก ต่ำกว่า แต่กำหนดผลได้แน่นอนเต็มที่
การใช้โดยทั่วไป ดึงข้อมูลจาก log, debug, สำรวจข้อมูล JSON Patch, อ้างตำแหน่งใน config, path ใน error

กฎการเลือกตรงไปตรงมา:

  • ต้อง "ดึง node ทั้งหมดที่ตรงเงื่อนไข" → JSONPath;
  • ต้อง "ระบุตำแหน่งเดียวให้แน่ชัดแล้วเพิ่ม ลบ หรือแก้ไข" → JSON Pointer;
  • ใช้ร่วมกัน: หา node ด้วย JSONPath แล้วอธิบายตำแหน่งของมันด้วย Pointer

อย่าใช้ JSONPath เพื่ออ้างตำแหน่งใน API contract JSONPath คืนค่าเป็นชุด และการแยกสามกรณีคือผลว่าง ผลหนึ่งตัว และหลายตัว ทำให้ protocol ซับซ้อน ส่วน Pointer หมายถึง "ชี้ไปยังตำแหน่งที่มีอยู่หรือไม่มีอยู่" ซึ่งเหมาะกับ contract มากกว่า

$.. กับ $.* ต่างกันอย่างไร?

$.. คือ recursive descent และไล่ลงไปทุกระดับ; $.* คืนเฉพาะลูกโดยตรงของระดับปัจจุบัน

{
  "a": { "id": 1, "child": { "id": 2 } },
  "b": { "id": 3 }
}
  • $.* → object สองตัวคือ {id:1, child:{...}} และ {id:3};
  • $..id → สามค่า: 1, 2, 3;
  • $.*.id → 1 และ 3 ไม่รวม 2 ที่ซ้อนอยู่

recursive descent สะดวกแต่แพง: $.. ไล่ทั้ง tree ซึ่งเป็น O(n) บนเอกสารขนาดใหญ่ และมันมักกวาดฟิลด์ลึกที่คุณไม่ได้ตั้งใจมาด้วย ถ้าใช้ระดับที่ชัดเจนได้ ก็เลี่ยง .. เถอะ

filter เปรียบเทียบ string ได้ไหม? จับคู่ด้วย regex ได้ไหม?

การเปรียบเทียบ string ทำได้ ส่วน regex ขึ้นกับ implementation

ไวยากรณ์มาตรฐาน (RFC 9535):

$.items[?(@.status == "active")]
$.items[?(@.price > 10 && @.stock > 0)]
$.items[?(@.name == $.target)]      // เปรียบเทียบกับ node อีกตัว

การเปรียบเทียบ string ใช้เครื่องหมายคำพูดคู่ (RFC 9535 อนุญาตเครื่องหมายคำพูดเดี่ยวด้วย แต่ implementation เก่าบางตัวรับเฉพาะเครื่องหมายคำพูดคู่) การเปรียบเทียบตัวเลขใช้ความหมายเชิงตัวเลข ถ้าเอา string มาปนกับ number เช่น "10" > 9 ถือเป็นพฤติกรรมที่มาตรฐานไม่ได้กำหนด จึงอย่าพึ่งมัน

สำหรับ regex นั้น RFC 9535 นิยามฟังก์ชัน match() และ search() ไว้ แต่การรองรับแตกต่างกันมากระหว่าง implementation วิธีที่ปลอดภัย: ให้ JSONPath กรองหยาบ ๆ แล้วค่อยไปจับคู่ regex ในโค้ดหลังได้ผลลัพธ์มาแล้ว

ควรใช้ JSON Patch หรือ Merge Patch?

ทั้งสองอธิบาย "จะเปลี่ยนอะไรอย่างไร" แต่ความสามารถในการแสดงออกและความหมายต่างกันโดยสิ้นเชิง

มิติ JSON Patch (RFC 6902) Merge Patch (RFC 7386)
รูปแบบ array ของ operation [{"op":"add",...}] object บางส่วน
operation ที่รองรับ add / remove / replace / move / copy / test merge เท่านั้น (ลบด้วย null)
อ้างสมาชิกของ array ได้ไหม ได้ ด้วย index ไม่ได้ ทั้ง array ถูกแทนที่
atomicity test เปิดใช้ optimistic locking ไม่มี
ขนาดเอกสาร ใหญ่แต่แม่นยำ เล็กแต่ความสามารถจำกัด
เหมาะกับ การแก้ไขละเอียด, การอัปเดต API บางส่วน การรวม config, การส่งฟอร์มบางส่วน

เกณฑ์ตัดสินใจ:

  • ต้องเปลี่ยนฟิลด์ของสมาชิกตัวที่สามใน array → JSON Patch;
  • ต้องรวมรายการ config หลายรายการเข้ากับ config ที่มีอยู่ → Merge Patch;
  • ต้องมีการควบคุม concurrency แบบ "ถ้าค่าเดิมไม่ใช่ X ให้ล้มเหลว" → test ของ JSON Patch

ระวังกับดักในความหมายของ null ของ Merge Patch: null หมายถึง "ลบ key นี้" ไม่ใช่ "ตั้งค่าเป็น null" การตั้งค่าเป็น null ทำไม่ได้ด้วย Merge Patch

2. การ validation ด้วย Schema

JSON Schema แก้ปัญหาอะไร?

มันเปลี่ยน "ข้อมูลควรมีหน้าตาอย่างไร" จากข้อตกลงด้วยวาจาเป็น contract ที่เครื่องตรวจได้ ข้อผิดพลาดที่มันกันได้จริง:

  • ฟิลด์ที่จำเป็นหายไป (required);
  • ชนิดผิด เช่นที่ควรเป็น number กลับเป็น string (type);
  • ค่านอกช่วง (minimum / maximum / minLength);
  • ค่านอกชุดที่อนุญาต (enum);
  • ชนิดของสมาชิกใน array ไม่สม่ำเสมอหรือความยาว array ผิด (items / minItems);
  • ฟิลด์ที่หายไปในโครงสร้างซ้อน (properties ช่วยให้ validation ไล่ลงไปได้);
  • ฟิลด์เกิน (additionalProperties: false)

สิ่งที่มัน แก้ไม่ได้: กฎทางธุรกิจ ("เวลาสิ้นสุดต้องมากกว่าเวลาเริ่ม"), เงื่อนไขข้ามฟิลด์ (บางกรณีเขียนด้วย if/then ได้ แต่จะอ่านเข้าใจยากขึ้น), ความไม่ซ้ำในฐานข้อมูล และการตรวจสิทธิ์

ดังนั้น Schema คือด่านตรวจ "รูปร่างของข้อมูล" ไม่ใช่ตัวแทนของ business logic

ควรใช้ draft-07 หรือ 2020-12?

โปรเจกต์ใหม่ใช้ 2020-12; ระบบที่มีอยู่แล้วให้ดูว่า validator รองรับอะไร

เวอร์ชัน สถานะ การเปลี่ยนแปลงสำคัญ
draft-04 ล้าสมัย ไลบรารีเก่าหลายตัวยังใช้เป็นค่าเริ่มต้น
draft-07 มาตรฐานโดยพฤตินัย รองรับกว้างที่สุด ระบบนิเวศสมบูรณ์ที่สุด
2019-09 ช่วงเปลี่ยนผ่าน เปิดตัว $defs, unevaluatedProperties
2020-12 แนะนำในปัจจุบัน แยก items เป็น prefixItems + items, รองรับ $dynamicRef

กับดักตอนย้ายสองข้อ:

  1. ไวยากรณ์ tuple เปลี่ยน: ใน draft-07 items ที่เป็น array ใช้แสดง tuple; 2020-12 ย้ายไปที่ prefixItems;
  2. $ref กับ keyword ข้างเคียง: ใน draft-07 keyword ข้างเคียงถูกเพิกเฉยเมื่อมี $ref; 2020-12 อนุญาตให้อยู่ร่วมกันได้

ถ้า validator รองรับแค่ draft-07 การเขียนไวยากรณ์ 2020-12 ลงไปจะถูกเพิกเฉยอย่างเงียบ ๆ — และกรณีที่อันตรายที่สุดคือ "validation ผ่าน" ทั้งที่ไม่ได้ตรวจอะไรเลย

จะสร้าง TypeScript type จาก Schema ได้อย่างไร?

สองเส้นทาง: สร้าง type จาก Schema หรือสร้าง Schema จาก type

จาก Schema ไป type (backend นิยาม contract, frontend นำไปใช้):

import type { FromSchema } from 'json-schema-to-ts';

const userSchema = {
  type: 'object',
  properties: {
    id: { type: 'string' },
    age: { type: 'integer' },
  },
  required: ['id'],
  additionalProperties: false,
} as const;

type User = FromSchema<typeof userSchema>;
// { id: string; age?: number }

หัวใจคือ as const: ถ้าไม่มีมัน type: 'string' จะถูกอนุมานเป็น string ไม่ใช่ literal 'string' และการอนุมานจะพัง

จาก type ไป Schema (frontend มี type อยู่แล้วและต้องการ validation ตอนรันไทม์):

import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';

const userSchema = z.object({ id: z.string(), age: z.number().int().optional() });
const jsonSchema = zodToJsonSchema(userSchema);

ข้อดีของเส้นทางนี้คือ "นิยามครั้งเดียวได้ทั้ง type และ validation ตอนรันไทม์"; ต้นทุนคือต้องเพิ่ม DSL และ Schema ที่สร้างออกมาอาจมีรายละเอียดของ implementation ติดมา (เช่นโครงสร้าง $ref ที่ดูไม่สวย)

จะหาฟิลด์ที่ผิดเมื่อ validation ล้มเหลวได้อย่างไร?

ดูที่ instancePath (2020-12) หรือ dataPath (draft-07) ที่ validator คืนมา มันคือ JSON Pointer ไปยังตำแหน่งที่ error

[
  {
    "instancePath": "/items/2/price",
    "schemaPath": "#/properties/items/items/properties/price/type",
    "keyword": "type",
    "message": "must be number"
  }
]

instancePath: "/items/2/price" บอกคุณตรง ๆ ว่า: ฟิลด์ price ของสมาชิกตัวที่สามใน array items มีชนิดผิด

ข้อแนะนำในทางปฏิบัติสองข้อ:

  1. คืน error ทั้งหมดในครั้งเดียว แทนที่จะ throw ที่ error แรก — frontend แก้ทีเดียวจบเร็วกว่าลองไปมาหลายรอบ;
  2. แปลง Pointer เป็นภาษาธรรมดา ก่อนแสดงให้ผู้ใช้เห็น; /items/2/price มีประโยชน์กับนักพัฒนาแต่ไร้ประโยชน์กับผู้ใช้

allErrors: true คือสวิตช์ใน validator ส่วนใหญ่ (Ajv ต้องเปิดเองอย่างชัดเจน); ค่าเริ่มต้นมักเป็นโหมด fail-fast

ควรเปิด additionalProperties: false ไหม?

เปิดสำหรับ contract ภายใน; ระวังเมื่อเป็น API สาธารณะ

เมื่อเปิดแล้ว ฟิลด์ใดที่ไม่ได้ประกาศใต้ properties จะทำให้ validation ล้มเหลว ข้อดีคือจับการพิมพ์ผิด (userNmae) และการส่งผ่านฟิลด์ที่ไม่คาดคิดได้; ข้อเสียคือการที่ upstream เพิ่มฟิลด์จะทำให้ downstream พัง และทำลายความเข้ากันได้ย้อนหลัง

ทางประนีประนอม:

สถานการณ์ ข้อแนะนำ
ระหว่างบริการภายใน เปิด การเปลี่ยนฟิลด์ต้องผ่านการจัดการเวอร์ชัน
API สาธารณะภายนอก ปิด ปล่อยให้ upstream เพิ่มฟิลด์ได้
การใช้ข้อมูลจากบุคคลที่สาม ปิด คุณไม่ได้ควบคุมอีกฝั่ง
การ validation config เปิด การพิมพ์ผิดใน config ควรล้มเหลวทันที

จะตรวจว่าสมาชิกใน array ไม่ซ้ำกันได้อย่างไร?

สองความหมายที่ต่างกัน — อย่าสับสน:

{ "uniqueItems": true }

อันนี้ deduplicate ทั้งสมาชิก โดยบังคับว่า array ต้องไม่มีสมาชิกสองตัวที่เท่ากันทุกชั้น

{
  "type": "array",
  "items": { "type": "object", "properties": { "id": { "type": "string" } } },
  "uniqueItems": true
}

ตัวอย่างข้างบนรับประกันเพียงว่า object ทั้งก้อนไม่ซ้ำกัน; {"id":"a","name":"x"} กับ {"id":"a","name":"y"} ไม่นับว่าซ้ำ

ถ้าต้อง deduplicate ตามฟิลด์ใดฟิลด์หนึ่ง Schema แสดงออกไม่ได้ ต้องตรวจในชั้นธุรกิจ:

const ids = items.map((item) => item.id);
if (new Set(ids).size !== ids.length) {
  throw new Error('id 重复');
}

format ตรวจ email กับวันที่จริงหรือไม่?

ไม่จำเป็น ในข้อกำหนด format เป็น "annotation" โดยค่าเริ่มต้น ไม่ใช่ "assertion" ดังนั้น validator อาจเก็บมันไว้โดยไม่ตรวจ

  • Ajv ต้องติดตั้งและตั้งค่าแพ็กเกจ ajv-formats แยก มิฉะนั้น format: "email" เท่ากับไม่มีผล;
  • validator บางตัวถือ format เป็น assertion ที่เปิดโดยค่าเริ่มต้น พฤติกรรมจึงไม่สม่ำเสมอ;
  • แม้เปิดใช้งาน regex ของ email ก็หลวมมาก — a@b ผ่านได้

สรุป: อย่าถือ format เป็นการ validation ที่เข้มแข็ง ให้ตรวจ email, URL และวันที่อย่างเข้มงวดในโค้ดธุรกิจของคุณ การเก็บ format ไว้ใน Schema เพื่อเป็นเอกสารก็คุ้ม แต่ไม่ต้องหวังให้มันกันข้อมูลสกปรก

3. สรุปเร็ว

คำถาม ข้อสรุป
หา node ตามเงื่อนไข JSONPath
อ้างตำแหน่งให้แม่นยำ, ใช้ patch JSON Pointer
เปลี่ยนสมาชิกของ array JSON Patch, Merge Patch ทำไม่ได้
รวม config Merge Patch
เวอร์ชัน Schema สำหรับโปรเจกต์ใหม่ 2020-12 ระวัง prefixItems
สร้าง TS type จาก Schema json-schema-to-ts + as const
หา error ของ validation อ่าน instancePath, เปิด allErrors
deduplicate ตามฟิลด์ Schema ทำไม่ได้ ใช้ Set ในชั้นธุรกิจ
format: email validator ส่วนใหญ่ไม่ตรวจโดยค่าเริ่มต้น อย่าพึ่งมัน