Zod 4 เปิดตัวแล้ว
(zod.dev)- Zod 4 เปิดตัวเป็นเวอร์ชันเสถียรหลังพัฒนามา 1 ปี แก้ข้อจำกัดด้านการออกแบบของ Zod 3 และปรับปรุง ประสิทธิภาพ ขนาดบันเดิล และประสิทธิภาพการคอมไพล์ TypeScript อย่างมาก
- Zod 3 ซึ่งออกเมื่อเดือนพฤษภาคม 2021 เติบโตจาก GitHub star 2,700 รายการและยอดดาวน์โหลดรายสัปดาห์ 600,000 ครั้ง มาเป็น 37.8k stars และ ยอดดาวน์โหลดรายสัปดาห์ 31 ล้านครั้ง ในปัจจุบัน และหลังผ่าน minor version 24 รุ่น การปรับปรุงสำคัญจำเป็นต้องมี breaking changes
- ใน benchmark การ parse สตริงเร็วขึ้น 14 เท่า, การ parse array เร็วขึ้น 7 เท่า, การ parse object เร็วขึ้น 6.5 เท่า และจำนวน type instantiation ของ
tscในตัวอย่างง่าย ๆ ลดจากมากกว่า 25,000 ครั้งเหลือประมาณ 175 ครั้ง - ขนาดบันเดิลหลักแบบ gzip ลดจาก Zod 3
12.47kbเหลือ Zod 4 เวอร์ชันปกติ5.36kbและ Zod Mini ซึ่งเป็น functional API ที่ tree-shaking ได้ ลดลงเหลือ1.88kb - ฟีเจอร์ใหม่รวมถึง การแปลงเป็น JSON Schema, metadata registry, การอนุมาน recursive object type, internationalization, error pretty-printing, template literal type,
z.stringbool(), พารามิเตอร์errorแบบรวมศูนย์ และz.discriminatedUnion()ที่ปรับปรุงแล้ว
เหตุผลที่ Zod 4 เป็น major version ใหม่
- Zod 4 กลายเป็นเวอร์ชันเสถียรหลังการพัฒนาอย่างต่อเนื่อง 1 ปี และนำฟีเจอร์ที่มีผู้ร้องขอมานานมาทำบนฐานที่เร็วกว่า เล็กกว่า และมีประสิทธิภาพกับ
tscมากกว่า - Zod v3.0 เปิดตัวในเดือนพฤษภาคม 2021 ตอนนั้นมี GitHub star 2,700 รายการ และยอดดาวน์โหลดรายสัปดาห์ 600,000 ครั้ง
- ปัจจุบัน Zod มี GitHub star 37.8k และยอดดาวน์โหลดรายสัปดาห์ 31 ล้านครั้ง
- เมื่อ 6 สัปดาห์ก่อนที่เบต้าออก ยอดดาวน์โหลดรายสัปดาห์อยู่ที่ 23 ล้านครั้ง
- เมื่อสะสม minor version มา 24 รุ่น codebase ของ Zod 3 ก็มาถึงขีดจำกัด และฟีเจอร์กับการปรับปรุงที่ถูกขอมากที่สุดจำเป็นต้องมี breaking changes
- Zod 4 ปิด public issue ที่ได้รับการโหวตมากที่สุด 9 จาก 10 รายการ
ประสิทธิภาพการ parse และประสิทธิภาพการคอมไพล์ TypeScript
- Zod 4 มี benchmark ที่รันได้โดยตรงจาก repository
- การปรับปรุงประสิทธิภาพการ parse:
- การ parse สตริง: เร็วขึ้น 14 เท่า
- การ parse array: เร็วขึ้น 7 เท่า
- การ parse object: เร็วขึ้น 6.5 เท่า
- benchmark การ parse object รัน Moltar validation library benchmark
- อิงจาก
tsc --extendedDiagnosticsการคอมไพล์ไฟล์ง่าย ๆ มีจำนวน type instantiation ลดลงมาก- เมื่อใช้
"zod/v3": มากกว่า 25,000 ครั้ง - เมื่อใช้
"zod/v4": ประมาณ 175 ครั้ง
- เมื่อใช้
- Zod 4 ออกแบบและทำให้ generics ของ
ZodObjectและ schema class อื่น ๆ เรียบง่ายขึ้น เพื่อหลีกเลี่ยง การพุ่งสูงของ instantiation - pattern ที่ chain
.extend()และ.omit()ซ้ำ ๆ ใน Zod 3 ใช้เวลาคอมไพล์4000msและการเรียก.extend()เพิ่มอาจทำให้เกิด error"Possibly infinite" - pattern เดียวกันใน Zod 4 คอมไพล์ได้ใน
400msจึงเร็วขึ้น 10 เท่า - ในอนาคต หากใช้ร่วมกับ compiler
tsgoประสิทธิภาพใน editor ของ Zod 4 อาจขยายไปถึง schema และ codebase ที่ใหญ่ขึ้นได้
ขนาดบันเดิลและ Zod Mini
- เปรียบเทียบขนาดบันเดิลหลักโดยบันเดิลสคริปต์ validation
z.boolean()แบบง่ายด้วยrollup - ขนาดบันเดิลหลักแบบ gzip:
- Zod 3:
12.47kb - Zod 4 เวอร์ชันปกติ:
5.36kb
- Zod 3:
- บันเดิลหลักของ Zod 4 เวอร์ชันปกติเล็กกว่า Zod 3 ประมาณ 57% หรือลดลง 2.3 เท่า
- API ของ Zod ที่เน้นเมธอดนั้นโดยพื้นฐานทำ tree-shaking ได้ยาก และแม้แต่สคริปต์
z.boolean()ง่าย ๆ ก็ยังดึง implementation ของเมธอดที่ไม่ได้ใช้ เช่น.optional(),.array()เข้ามาด้วย - Zod Mini มี API แบบ functional ที่สอดคล้องกับ
zodแบบ 1:1 และสามารถ tree-shaking ได้- ในจุดที่ Zod ใช้เมธอด Zod Mini มักใช้ wrapper function
- เมธอดสำหรับ parse ของ Zod และ Zod Mini เหมือนกัน
- เพิ่ม refinement ด้วยเมธอด
.check()อเนกประสงค์
- ยังแนะนำให้ใช้ Zod ปกติต่อไปสำหรับ use case ส่วนใหญ่ ส่วนโปรเจกต์ที่มีข้อจำกัดขนาดบันเดิลเข้มงวดผิดปกติอาจพิจารณา Zod Mini
- เมื่อใช้
zod/miniขนาดบันเดิลแบบ gzip คือ1.88kb- ลดลง 85% หรือ 6.6 เท่าเมื่อเทียบกับ Zod 3
- ตารางเปรียบเทียบ: Zod 3
12.47kb, Zod 4 ปกติ5.36kb, Zod 4 Mini1.88kb
- ดูรายละเอียดเพิ่มเติมได้ในเอกสาร
zod/mini
Metadata และ JSON Schema
- Zod 4 นำระบบใหม่สำหรับเพิ่ม metadata แบบ strongly typed ให้กับ schema
- metadata ไม่ได้เก็บไว้ใน schema เอง แต่เก็บใน schema registry ที่เชื่อม schema กับ typed metadata
- สร้าง registry ได้ด้วย
z.registry()และลงทะเบียนแบบสะดวกผ่านเมธอด.register()ของ schema ได้ด้วย - Zod export global registry ชื่อ
z.globalRegistryที่รับ metadata ทั่วไปซึ่งเข้ากันได้กับ JSON Schema - เมธอด
.meta()ช่วยเพิ่ม schema เข้าz.globalRegistryได้สะดวก - เพื่อความเข้ากันได้กับ Zod 3 ยังมี
.describe()อยู่ แต่ใน Zod 4 แนะนำให้ใช้.meta() - Zod 4 มี การแปลงเป็น JSON Schema แบบ first-party ผ่าน
z.toJSONSchema() - metadata ใน
z.globalRegistryจะถูกรวมเข้าใน output ของ JSON Schema โดยอัตโนมัติ - ข้อมูลการปรับแต่ง JSON Schema ที่สร้างขึ้นอยู่ใน JSON Schema docs
การแสดง schema และความสามารถด้าน type ใหม่
- Zod 4 สามารถอนุมาน recursive object type ได้อย่างเหมาะสม
- สามารถแสดง recursive type และ mutually recursive type ได้
- ไม่เหมือน pattern recursive type ของ Zod 3 ตรงที่ไม่ต้อง type casting
- schema ที่ได้เป็น instance ของ
ZodObjectปกติ และใช้เมธอดทั้งหมดได้
- เพิ่ม File schema สำหรับ validate instance ของ
File z.templateLiteral()แสดง template literal type ของ TypeScript- type ของ Zod schema ที่แปลงเป็นสตริงได้จะเก็บ regular expression ภายใน
- เป้าหมายรวมถึงสตริง, string format เช่น
z.email(), number, boolean, bigint, enum, literal, undefined/optional, null/nullable และ template literal อื่น - constructor
z.templateLiteralจะนำสิ่งเหล่านี้มาต่อกันเป็น super-regex เดียว - string format เช่น
z.email()จะถูกบังคับใช้อย่างถูกต้อง แต่ custom refinement จะไม่ถูกบังคับใช้ - รายละเอียดเพิ่มเติมอยู่ใน template literal docs
- เพิ่ม number format ใหม่สำหรับแสดง integer แบบ fixed-width และ floating point type
- คืน instance ของ
ZodNumberที่เพิ่ม constraint แบบ inclusive minimum/maximum ที่เหมาะสมไว้แล้ว
- คืน instance ของ
- เพิ่ม bigint number format ที่เกินช่วงที่ JavaScript
numberแสดงได้อย่างปลอดภัย- คืน instance ของ
ZodBigIntที่เพิ่ม constraint แบบ inclusive minimum/maximum ที่เหมาะสมไว้แล้ว
- คืน instance ของ
- ตอนนี้
z.literal()รับได้หลายค่าแบบเลือกได้
Error, internationalization และ string format
- Zod 4 นำ
localesAPI ใหม่มาใช้สำหรับแปล error message ทั่วทั้งระบบเป็นหลายภาษา - รายชื่อ locale ที่รองรับทั้งหมดอยู่ใน Customizing errors และจะอัปเดตเมื่อเพิ่มภาษาที่รองรับ
- Zod implement function ระดับบนสุด
z.prettifyErrorที่แปลงZodErrorเป็นสตริงในรูปแบบที่เป็นมิตรต่อผู้ใช้- ปัจจุบันรูปแบบยังตั้งค่าไม่ได้
- อาจเปลี่ยนแปลงได้ในอนาคต
- หากใช้งาน
zod-validation-errorอยู่แล้ว ก็ยังใช้ต่อได้
- string format ทั้งหมด เช่น email ถูกยกระดับเป็น function ระดับบนสุดของโมดูล
z- กระชับกว่าและเป็นมิตรต่อ tree-shaking มากกว่า
- API แบบเมธอดที่สอดคล้องกัน เช่น
z.string().email()ยังใช้งานได้ แต่ถูก deprecated แล้ว - มีกำหนดจะถูกลบใน major version ถัดไป
- API
z.email()รองรับ custom regular expression- ไม่มี canonical email regex ที่ตายตัวเพียงหนึ่งเดียว และแต่ละแอปพลิเคชันอาจเลือกเกณฑ์ที่เข้มงวดหรือผ่อนปรนต่างกัน
- เพื่อความสะดวก Zod export regular expression ทั่วไปไว้หลายแบบ
การแปลง boolean และการทำ error customization ให้ง่ายขึ้น
z.coerce.boolean()เดิมเป็น API ง่าย ๆ ที่แปลงค่า falsy เป็นfalseและค่า truthy เป็นtruefalse,undefined,null,0,"",NaNฯลฯ จะกลายเป็นfalse- พฤติกรรมนี้สอดคล้องกับ API
z.coerceอื่น ๆ
- Zod 4 นำ
z.stringbool()มาใช้เพื่อ boolean coercion แบบละเอียดขึ้นในสไตล์ env- สามารถปรับแต่งค่า truthy และ falsy ได้
- รายละเอียดเพิ่มเติมอยู่ใน
z.stringbool()docs
- breaking changes ส่วนใหญ่ของ Zod 4 เกี่ยวข้องกับ API สำหรับปรับแต่ง error
- การปรับแต่ง error ถูกรวมเป็นพารามิเตอร์
errorเดียวmessageถูกแทนที่ด้วยerror- พารามิเตอร์
messageยังรองรับอยู่ แต่ถูก deprecated แล้ว invalid_type_errorและrequired_errorถูกแทนที่ด้วยerrorในไวยากรณ์แบบ functionerrorMapก็ถูกแทนที่ด้วยerrorในไวยากรณ์แบบ function เช่นกัน
Composability, refinement และ transformation
z.discriminatedUnion()รองรับ schema type หลายแบบที่ก่อนหน้านี้ไม่รองรับ- รวมถึง union และ pipe
- สามารถใช้ discriminated union เป็น member ของ discriminated union อีกตัวได้ จึง ประกอบกันได้
- ใน Zod 3 refinement ถูกเก็บไว้ใน class
ZodEffectsที่ห่อ schema ต้นฉบับ- โครงสร้างนี้ทำให้ใช้
.refine()ร่วมกับเมธอด schema อื่น ๆ เช่น.min()ได้ไม่สะดวก
- โครงสร้างนี้ทำให้ใช้
- ใน Zod 4 refinement ถูกเก็บไว้ภายใน schema ทำให้ใช้
.refine()ร่วมกับเมธอด schema อื่น ๆ ได้อย่างเป็นธรรมชาติ - เมธอดใหม่
.overwrite()แสดง transform ที่ไม่เปลี่ยน inferred type.transform()เป็น black box ที่ output type ไม่สามารถ introspect ได้ตอน runtime และ transform function อาจคืนค่าอะไรก็ได้- ด้วยเหตุนี้จึงไม่มีวิธีแปลงเป็น JSON Schema ได้อย่างปลอดภัย
.overwrite()คืน instance ของ class เดิม- overwrite function ถูกเก็บเป็น refinement และไม่แก้ไข inferred type
.trim(),.toLowerCase(),.toUpperCase()เดิมถูก implement ใหม่โดยใช้.overwrite()
ฐานสำหรับผู้เขียนไลบรารี
- การเพิ่ม Zod Mini ทำให้มี subpackage
zod/v4/coreซึ่งรวมฟังก์ชันหลักที่ Zod และ Zod Mini ใช้ร่วมกัน zod/v4/coreขยาย Zod จากไลบรารีธรรมดาให้เป็นฐาน validation ที่รวดเร็วและฝังใช้ในไลบรารีอื่นได้- หากสร้าง schema library สามารถอ้างอิง implementation ของ Zod และ Zod Mini แล้วสร้างบน
zod/v4/coreได้ - มีคู่มือ For library authors สำหรับผู้เขียนไลบรารี
- ครอบคลุม best practice เมื่อสร้างบน Zod
- ตอบคำถามที่พบบ่อยเกี่ยวกับวิธีรองรับ Zod 3, Zod 4 และ Mini พร้อมกัน
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ผมเป็นผู้เขียนเอง ถามมาได้ทุกอย่างครับ ส่วนเรื่องนโยบายเวอร์ชัน ผมสรุปเหตุผลของแนวทางนี้ไว้อย่างค่อนข้างละเอียดที่นี่: https://github.com/colinhacks/zod/issues/4371
ท้ายที่สุดแล้ว npm ไม่ได้ถูกออกแบบมาให้รับมือกับสถานการณ์แบบที่ Zod เจอได้ดี มีไลบรารีจำนวนหลายสิบถึงหลายร้อยตัวที่นำเข้าอินเทอร์เฟซ/คลาสของ Zod โดยตรงแล้วเอาไปใช้ใน API สาธารณะของตัวเอง ดังนั้นทุกครั้งที่ Zod ขึ้น major version ไลบรารีเหล่านั้นก็ต้องออก major version ใหม่ด้วย
ถ้ามองแยกเดี่ยว ๆ ก็ดูสมเหตุสมผล แต่สำหรับ Zod มันอาจทำให้เกิด หิมะถล่มของเวอร์ชัน ที่เจ็บปวดสำหรับทุกคน และดูเหมือนว่าส่วนใหญ่ของอีโคซิสเต็มจะถูกตรึงอยู่กับ v3 ไปตลอด
ดังนั้นจึงเลือกแนวทางคล้ายกับ Go คือแทนที่แพ็กเกจจะออกเวอร์ชันใหม่ที่ทำให้ความเข้ากันได้พัง ก็เพิ่ม subpath ใหม่แทน ในอีโคซิสเต็ม TypeScript ไลบรารีสามารถมี peer dependency เพียงตัวเดียวคือ
zod@^3.25.0แล้วนำเข้าสิ่งที่ต้องใช้จาก"zod/v3"และ"zod/v4"เพื่อรองรับทั้งสองเวอร์ชันพร้อมกันได้แต่ถึงจะเข้าใจเหตุผลที่เลือกนโยบายเวอร์ชันที่ค่อนข้างแปลกเพราะมีเงื่อนไขพิเศษ ผมก็ยังอยากให้มีแพ็กเกจ
4.0.0ด้วย สำหรับทีมที่ไม่ต้องกังวลเรื่องเวอร์ชัน Zod ชนกันใน transitive dependencyถ้าผมเข้าใจถูก ต้องเปลี่ยน import เป็น
"zod/v4"ซึ่งเป็นการเปลี่ยนที่มีเสียงรบกวนสูงมาก และอาจปวดหัวเพราะต้องใช้กฎ lint มาจับปัญหาที่ IDE auto import จาก'zod'.optional()ใน TypeScript รวมอยู่ในการแก้ issue 9–10 อันดับแรกหรือไม่: https://github.com/colinhacks/zod/issues/635นี่เป็นความเจ็บปวดที่ใหญ่ที่สุดของผมใน Zod แต่ Zod ดีมากจนยังยอมรับและใช้งานต่ออยู่
อยากรู้ว่ามองว่านี่อยู่นอกขอบเขตของ Zod หรือแค่ยังไม่มีเวลานำไป implement การอภิปรายที่เกี่ยวข้องอยู่ที่ https://github.com/colinhacks/zod/discussions/2134
3.25.0แบบนี้ฟังดูไม่ค่อยสมเหตุสมผลเท่าไรผมใช้ Zod alpha มาหลายเดือนแล้ว และแค่อยากแก้
package.jsonเพื่ออัปเกรด แต่ตอนนี้ดูเหมือนต้องไล่ค้น git history ทั้งหมดผมซาบซึ้งกับโปรเจกต์นี้มาก แต่ feedback ข้อนี้ดูเหมือนถูกทำให้ยากเกินจำเป็น น่าจะปล่อย 4.x ควบคู่กับ
3.xได้หรือเปล่าข้อความที่ว่า “เพื่อทำให้การ migrate ง่ายขึ้น จะเผยแพร่ Zod 4 เป็นส่วนหนึ่งของ release
zod@3.25คู่กับ Zod 3 และให้นำเข้าจาก subpath"/v4"” ดูเหมือนเป็นสัญญาณว่า การจัดการ dependency ของ npm เละเทะไปหมดpeer dependency พังจนทำให้ v4 ต้องแสร้งทำเป็น v3
เห็นด้วยว่า npm จัดการสถานการณ์ที่ Zod เจอได้ไม่ดีนัก แต่ Zod อยู่ภายใต้ข้อจำกัดที่แทบไม่มีไลบรารีไหนเจอ มีไลบรารีจำนวนหลายสิบถึงหลายร้อยตัวที่นำเข้าอินเทอร์เฟซ/คลาสจาก
"zod"โดยตรงแล้วใช้ใน API สาธารณะของตัวเองไลบรารีแบบนี้ต้องออก major version ใหม่ทุกครั้งที่ Zod ขึ้น major version และสำหรับ Zod สิ่งนั้นอาจก่อให้เกิด หิมะถล่มของเวอร์ชัน ที่เจ็บปวดสำหรับทุกคน พูดตรง ๆ ผมคิดว่าส่วนใหญ่ของอีโคซิสเต็มน่าจะถูกตรึงไว้กับ v3 ไปตลอด
ดังนั้นจึงเลือกวิธีคล้ายกับ Go คือไม่ออกแพ็กเกจเวอร์ชันใหม่ทุกครั้งที่มี release ที่ทำให้ความเข้ากันได้พัง แต่เพิ่ม subpath ใหม่แทน ในอีโคซิสเต็ม TypeScript สามารถรองรับ
"zod/v3"และ"zod/v4"พร้อมกันได้ด้วย peer dependency ตัวเดียวคือzod@^3.25.0กล่าวคือ refactor จุดใช้งานทีละจุดให้เป็น
import ... from 'zod/v4'แล้วพอเสร็จก็อัปขึ้น v4 อย่างเต็มตัวตรงกันข้าม npm ยังมีทางออกมากกว่า เพราะถ้าจำเป็นก็สามารถรันหลายเวอร์ชันพร้อมกัน หรือเลือก override บางส่วนของกราฟ transitive dependency ได้
ระบบจัดการแพ็กเกจไหนแก้ปัญหานี้ได้บ้าง? แม้แต่แพลตฟอร์มที่ “เสถียร” อย่าง Maven ก็จัดการปัญหาแบบนี้ด้วยการออกเวอร์ชันใน namespace ใหม่ เช่นตอน Apache Commons ย้ายจาก v2 ไป v3
มีคำถามที่สงสัยมานาน เคยได้ยินว่า Zod อาจเหมาะกับกรณีนี้ แต่พอดูเอกสารแล้วก็ยังไม่ค่อยรู้ว่าควรทำอย่างไร
สมมติว่า API ฝั่งเซิร์ฟเวอร์คืนค่าชนิดที่ละเอียดกว่าสิ่งที่มันสามารถแสดงออกได้ เช่น
api/:postid/authorคืนค่า User แต่ User นั้นอาจเป็น User ปกติหรือ User นิรนามก็ได้ ทำให้ฟิลด์อย่างusername,locationอาจมาเป็น null ในกรณีนี้เราอาจอยากแสดงอ็อบเจ็กต์ User เป็น discriminated unionอ็อบเจ็กต์ที่มาจาก endpoint อื่นก็อาจต้องมีการแปลงชนิดเช่นกัน User บางครั้งอาจมี
Post[]รวมอยู่ และถ้า Post เป็นโพสต์ของ moderator ก็อาจมีพร็อพเพอร์ตีเฉพาะได้เมื่อก่อนเคยเขียนฟังก์ชันอย่าง
normalizeUser()กับnormalizePost()แต่ไม่นานก็เละเทะ เพราะแต่ละ endpoint คืนค่าสับเซ็ตที่ต่างกันของโมเดล User/Post จนต้องสร้างnormalizePostประมาณ 5 ตัว ซึ่งวุ่นวายมาก อยากรู้ว่าปกติปัญหาแบบนี้แก้กันอย่างไรเช่น แยกอ็อบเจ็กต์
successกับfailedตามฟิลด์ที่ใช้แยกแยะอย่างstatusถ้ามีพร็อพเพอร์ตีพิเศษสำหรับ moderator เยอะ แต่ไม่อยากไล่ระบุหรือตรวจทั้งหมด ก็สามารถกำหนดพฤติกรรมแบบ pass-through ได้ต่อให้หลายเมธอดมี schema ต่างกัน ถ้ามีส่วนร่วมกัน ก็สร้าง object schema ที่ใช้ร่วมกัน แล้วสร้างอ็อบเจ็กต์อื่น ๆ ที่เพิ่มฟิลด์เข้าไปได้ ถ้าความเป็นไปได้มีเยอะก็ยังอาจรกอยู่ดี แต่ถ้าสถานการณ์เดิมก็รกอยู่แล้ว อย่างน้อย validator ก็ช่วยตรวจความรกนั้นให้ได้
ถ้าไม่ใช่ full-stack TypeScript เช่นแบ็กเอนด์ Python ก็อาจนิยามรูปแบบ
Userหลายแบบด้วยโมเดลและ union ของ Pydantic แล้วสร้างสคีมา OpenAPI/GraphQL พร้อมทำ code generation เพื่อสร้าง TypeScript client type"user_type"ให้กับทุกชนิดใน union ก็จะจัดการกรณีทั่วไปและกรณีพิเศษได้ง่ายขึ้นเช่น ถ้าตรวจ
user.user_type === 'authenticated'ระบบชนิดก็จะรู้ว่าหลังจากนั้นสามารถใช้user.nameได้ไลบรารี TypeScript สำหรับ query ของ GraphQL สามารถอนุมานรูปแบบ response แบบไดนามิกจากฟิลด์ที่เลือกได้ ถ้าเลือก
{ user { username posts { text } } }ก็จะได้ชนิด response ที่มีpostsและถ้าเลือกแค่{ user { username } }พร็อพเพอร์ตีpostsก็จะหายไปโดยสิ้นเชิงเซิร์ฟเวอร์รู้ข้อมูลที่ตัวเองส่งอยู่แล้ว จึงควรให้ข้อมูลนั้นกับ client ในรูป metadata ในเชิงปฏิบัติ แบ็กเอนด์ Python อาจใช้สคีมา Pydantic แล้วสร้างสเปก OpenAPI อัตโนมัติจากสิ่งนั้น จากนั้นให้ client สร้าง type ขึ้นมา
Zod ดีกว่าทางเลือกอื่น ๆ ที่เคยเห็นมาก แต่บางครั้งข้อเท็จจริงที่ว่าเราจำเป็นต้องมี การตรวจสอบแบบ explicit แบบนี้เอง ก็ให้ความรู้สึกเหมือนเป็นความล้มเหลวของพัฒนาเว็บสมัยใหม่ว่ามาถึงจุดไหนแล้ว
ในการพัฒนาแบบ full-stack มันน่าหงุดหงิดมากที่ต้องอธิบายรูปทรงเดียวกันหลายวิธี ต้องมีการตรวจ input ใน JS, Swagger สำหรับนิยาม API, การตรวจ input ฝั่งเซิร์ฟเวอร์, ORM เพื่อให้สอดคล้องกับ schema และยังต้องมีนิยาม TypeScript แยกกันระหว่างเซิร์ฟเวอร์กับ client อีก น่าเบื่อมาก
TypeScript ให้ความรู้สึกเหมือนเป็น single source of truth ที่ดีที่สุดที่เรามี แต่ในความเป็นจริง เครื่องมือที่พยายามทำ reflection ต่างก็นิยามโมเดลและ builder ของตัวเองซ้ำขึ้นมาใหม่ มีโดเมนใหญ่ ๆ ที่ reflection เป็นแกนหลักอยู่ถึง 5 แห่ง และแต่ละแห่งก็มี implementation หลายตัว
เครื่องมือปล่อย type ของ TypeScript แบบเก่าอย่าง
reflect-metadataให้ข้อมูล runtime type ได้บางส่วน แต่เล็งไปที่ decorator และสเปก metadata ที่เก่ามาก: https://www.npmjs.com/package/reflect-metadataบางทีเราอาจเพิ่งอยู่ช่วงเริ่มต้นของการรวมศูนย์ก็ได้ โปรเจกต์ Standard Schema ดูเหมือนจะได้รับการสนับสนุนจากไลบรารีตรวจสอบหลัก ๆ ส่วนใหญ่ แต่การขยายไปถึงเครื่องมือนิยาม API หรือ ORM ยังอยู่ในระยะเริ่มต้นมาก
https://github.com/standard-schema/standard-schema?tab=readm...
เปลี่ยนใน Zod schema ครั้งเดียว ก็แพร่ไปทั่วทั้งแอปพร้อมกับการตรวจ type เท่ากับว่า Zod schema กลายเป็น single source of truth
ถ้ามี JSON Schema/Swagger schema อยู่แล้ว ก็สร้าง Zod จากตรงนั้นได้ ถ้าใช้ TypeScript ORM ก็แทบจะแน่นอนว่ามี Zod generator
พูดตรง ๆ คือ Zod ถูกใช้กว้างขวางมากจนสำหรับผม มันกลายเป็น นิยาม schema แบบรวมศูนย์ ที่ส่วนอื่น ๆ ของ stack พึ่งพาได้ เพราะนิยาม type โดยตรง นักพัฒนาจึงรักษาให้เป็นปัจจุบันจริง ๆ เอกสาร Swagger ของบริษัทมักตามหลังการเปลี่ยนแปลงเสมอ
กรณีที่การคงสภาพเดิมพอใช้ได้ ก็มีแค่เวลารับมือกับ client และนายจ้างที่ต้องการแค่ผลลัพธ์ ไม่สนใจวิธีทำเท่านั้น ในบริบทแบบนั้น ชั้นความซับซ้อนที่น่าเศร้าเหล่านี้อย่างน้อยก็ช่วยเพิ่มชั่วโมงที่เรียกเก็บเงินได้
แน่นอนว่าอยู่บนสมมติฐานว่า Zod คือไลบรารีนิยาม schema และตรวจสอบของ TypeScript ที่ตนเลือกใช้
Zod 4 ดูน่าสนใจ แต่ถึงจะรวมการปรับปรุงล่าสุดแล้ว ArkType ก็ยังเร็วกว่าอยู่หลายเท่าตัวในระดับเลขหลักเดียว
บางครั้งเพื่อรักษาความเข้ากันได้ย้อนหลังและความเข้ากันได้ของไวยากรณ์ ก็ทำให้ยากที่จะทำให้เร็วได้เท่ากับไลบรารีที่เริ่มใหม่ทั้งหมด ผมเพิ่งวิเคราะห์เครื่องมือเหล่านี้ทั้งหมดในโปรเจกต์ล่าสุด และเลือก ArkType ส่วนหนึ่งก็เพราะเหตุผลนี้ อีกอย่างคือรู้สึกว่าประสบการณ์การใช้งานกับ TypeScript ดีกว่าด้วย
เราใช้ Zod แค่ตรวจสอบความถูกต้องของฟอร์ม เลยคิดว่า “ทำไมเรื่องนี้ถึงสำคัญ?” สงสัยว่ามีกรณีที่ผู้คนตรวจสอบความถูกต้องของข้อความอินพุต API ที่มี throughput สูง ๆ จนประสิทธิภาพสำคัญขึ้นหรือเปล่า
ถึงอย่างนั้นก็คงไม่ย้ายออกจาก Zod
ขอแสดงความยินดีกับทีม Zod สำหรับรีลีสใหม่ แม้จะเสี่ยงฟังดูแง่ลบเกินไป แต่จำนวน การเปลี่ยนแปลงที่ทำให้เข้ากันไม่ได้ ในคู่มือไมเกรชันทำให้รู้สึกขนลุก
ถ้าเป็นโปรเจกต์ที่พึ่งพา Zod มาก น่าจะเป็นงานหนักที่ต้องใช้สมาธิและเวลาของนักพัฒนาเยอะมาก ผมยิ่งเข้าใจเป็นพิเศษเพราะเคยดูแลโปรเจกต์ฟรอนต์เอนด์อายุ 4–5 ปีอยู่หลายตัวในที่ทำงาน
โปรเจกต์ React ขนาดใหญ่มักพึ่งพาไลบรารีจำนวนมาก และเมื่อแต่ละไลบรารีออกการเปลี่ยนแปลงใหญ่ ๆ แถมเอกสารยังไม่พอ ก็กลายเป็นเรื่องท่วมท้นได้อย่างรวดเร็ว นี่เป็นหนึ่งในส่วนที่ผมเกลียดที่สุดเวลาทำงานกับ JavaScript และการทำให้ทุกอย่างเข้ากันและยังทำงานลื่นไหลต่อไปได้รู้สึกเหมือนการเดินขึ้นเขาที่ไม่มีวันจบ
พูดตรง ๆ คือเหมือนฝันร้าย และคิดเลยว่าน่าจะสร้างด้วย Django ไปดีกว่า ผิดคาดมากที่การไมเกรต Tailwind 3 → 4 เป็นหนึ่งในส่วนที่เจ็บปวดที่สุด
miniแยกต่างหาก ทำให้ไมเกรตแบบค่อยเป็นค่อยไปได้ง่ายโดยไม่ต้องไปยุ่งกับ package managerผู้ใช้ที่ไม่สนใจเอดิชัน
miniก็ไม่จำเป็นต้องสนใจส่วนนั้น แต่ประโยชน์ด้าน tree shaking ของเอดิชันminiใหญ่เกินไปจนกระตุ้นให้เกิดการพัฒนาทางเลือกอื่น ๆ และ Zod ก็ต้องยอมรับมัน ไม่อย่างนั้นก็เสื่อมความนิยมลงในฐานะนักพัฒนา full-stack ที่ดูแล SaaS ในโปรดักชันเองและมีผู้ใช้หลายพันคน บางครั้งผมรู้สึกดีที่ได้ตระหนักว่ามีปัญหาอีกมากแค่ไหนที่ผมไม่รู้เลย เพราะตัดสินใจไม่สร้าง SPA และไม่ใช้เฟรมเวิร์กฟรอนต์เอนด์ JS
ผมไม่เคยแม้แต่คิดว่าต้องมีอะไรอย่าง Zod/ArkType เพิ่งเคยได้ยินครั้งแรก และก็ไม่มีเหตุให้ต้องใช้
ผมทึ่งกับปริมาณความพยายาม ความซับซ้อน และเครื่องมือที่ต้องใช้ในการสร้าง SPA ถ้าตัดสินใจไม่สร้าง SPA ปัญหาทั้งกลุ่มหนึ่งก็หายไปเลย แทนที่จะทำอย่างนั้น ก็ใช้เบราว์เซอร์ตามที่มันถูกออกแบบมาแต่เดิม คือรีเฟรชทั้งหน้าและเน้นพัฒนาแบ็กเอนด์ แล้วค่อยเพิ่มความเป็น reactive เป็นครั้งคราวด้วยเฟรมเวิร์กฝั่งแบ็กเอนด์ล้วน ๆ อย่าง Laravel Livewire
ทุกอย่างตั้งแต่การควบคุมการเข้าถึง การตรวจสอบความถูกต้อง ไปจนถึงการจัดการ state ล้วนเรียบง่าย แอปเร็ว ตอบสนองดี ทันสมัย เป็นมิตรกับ SEO และกำลังให้บริการผู้ใช้หลายพันคนในโปรดักชัน
สุดท้ายแล้ว schema ก็ต้องอยู่ที่ไหนสักแห่ง อาจนิยามอยู่ในระบบฐานข้อมูล หรือในโมเดล ORM ก็ได้ แต่ในโปรเจกต์ใหญ่ การวาง schema ไว้ในระบบฐานข้อมูลหรือ ORM อาจก่อปัญหาที่ยากมากได้ เราจึงใช้ Zod และ Protobuf ผมมองว่า Protobuf เองก็สามารถถูกแทนที่ทั้งหมดด้วย Zod หรือสิ่งคล้าย ๆ กันได้
คดีอื้อฉาวของ Post Office ในสหราชอาณาจักรเกี่ยวข้องกับระบบ IT ชื่อ Horizon: https://en.wikipedia.org/wiki/British_Post_Office_scandal
หลังอ่านรายละเอียดแล้ว ผมคิดว่าถ้ามี การตรวจสอบ schema แบบ Zod ก็น่าจะมีส่วนช่วยป้องกันคดีอื้อฉาวนี้ได้ ภาคผนวกทางเทคนิคของเอกสารศาลในคดีแบบกลุ่มก็กล่าวถึงการไม่มีการใช้ schema เช่นกัน: https://en.wikipedia.org/wiki/Bates_%26_Others_v_Post_Office...
ตอนนี้ฟังดูเหมือนเขียน
ifแบบ defensive จำนวนมาก หรือแย่กว่านั้นคืออาจพึ่งพาแค่การตรวจสอบฟอร์มฝั่งไคลเอนต์ด้วย HTMLไม่ใช่ผู้เชี่ยวชาญจึงต้องพูดอย่างระมัดระวัง แต่คิดว่า JSON Schema อาจเป็นตัวเลือกที่ดี เพราะเป็นแบบอิงสคีมา จึงสามารถทำตัวตรวจสอบความถูกต้องในภาษาอื่นที่ไม่ใช่ TypeScript ได้
https://ajv.js.org เป็นหนึ่งในไลบรารี JSON Schema แบบนั้น อยากรู้ว่าเมื่อเทียบกับ Zod แล้วเป็นอย่างไร
ถ้าต้องการก็ใช้เป็นตัวแปลง JSON ได้ด้วย เช่น เขียนสคีมาที่รับสตริง ตรวจสอบว่าเป็นสตริงวันที่ ISO หรือไม่ แล้วส่งออกเป็นออบเจกต์
Dateได้ความแตกต่างสำคัญคือการประมวลผลล่วงหน้าและ
refineใน Zod สามารถให้คอลแบ็กก่อนการตรวจสอบได้ ซึ่งมีประโยชน์มาก แต่ไม่สามารถแสดงเป็น JSON ได้ ตัวอย่างเช่น การแปลงMM/DD/YYYYเป็นDD/MM/YYYYก่อนตรวจสอบวันที่ เป็นสิ่งที่มีประโยชน์บ่อยกว่าที่คิดสำหรับการตรวจสอบจะใช้ AJV หรือระบบตรวจสอบสคีมาของ TypeBox เองก็ได้ อย่างหลังเร็วกว่าอย่างมาก แต่รองรับเฉพาะแบบซิงโครนัส ส่วน AJV มีตัวเลือกตรวจสอบแบบอะซิงโครนัส เช่น การตรวจสอบกับฐานข้อมูล
แปลกใจกับปฏิกิริยาเชิงลบรอบ ๆ ประเด็นนี้
เคยทดสอบ Zod v4 เวอร์ชันแรก ๆ และชอบ API ใหม่ แต่กังวลมากว่าเส้นทางการย้ายเวอร์ชันจะเป็นอย่างไร ถึงขั้นคิดจะเสนอให้เผยแพร่ด้วยชื่อแพ็กเกจใหม่ไปเลย
แต่แนวทางของผู้เขียนนั้นฉลาด ทำให้คนอย่างผมสามารถ เริ่มใช้ v4 ได้ทันที โดยไม่ต้องรอให้ dependency ทั้งหมดอัปเดตก่อน
เพิ่งเริ่มนำ Zod มาใช้ในโปรเจกต์ใหม่ และไม่มีจังหวะไหนจะดีไปกว่านี้แล้ว
จากสิ่งที่เห็นตอนนี้ ถ้าจะย้ายไป v4 คงมีสิ่งที่ต้องแก้เยอะมากจริง ๆ