JSON إلى Zod
يولّد مخططات Zod من JSON مع نوع z.infer لكل منها: عناصر المصفوفة مدمجة، والمفاتيح الاختيارية والتي تقبل null معلّمة، ولا يُخمَّن شيء من العيّنة.
import * as z from "zod"
export const Customer = z.object({
name: z.string(),
email: z.string(),
phone: z.null(),
})
export type Customer = z.infer<typeof Customer>
export const LineItem = z.object({
sku: z.string(),
quantity: z.number(),
price: z.number(),
note: z.string().nullable(),
backordered: z.boolean().optional(),
})
export type LineItem = z.infer<typeof LineItem>
export const Root = z.object({
id: z.string(),
customer: Customer,
lineItems: z.array(LineItem),
tags: z.array(z.unknown()),
})
export type Root = z.infer<typeof Root>
صياغة Zod 4 وهي صالحة في Zod 3 كذلك. يُسقط z.object كل مفتاح لا يذكره، فالحقل الذي غاب عن عيّنتك يُحذف بصمت من البيانات التي يحلّلها.
في كل موضع أدناه لم تحمل عيّنتك سوى null، لذا لا يقبل المخطط هناك أي شيء آخر.
المواضع: 1
Customer.phone
في كل موضع أدناه كان كل رقم عددًا صحيحًا. يقبل z.number() أيضًا 7.5، وكان .int() سيشترط أعدادًا صحيحة، لذا أضفه بنفسك فقط حيث تعلم أن القيمة يجب أن تكون عددًا صحيحًا.
المواضع: 1
LineItem.quantity
في كل موضع أدناه تعذّر استنتاج أي شيء، لذا لا يتحقق المخطط مما يوجد هناك: يقبل z.unknown() أي قيمة، ويقبل z.record(z.string(), z.unknown()) أي كائن.
المواضع: 1
Root.tags[]
تحقق يجري كلما وصلت البيانات
يُفحص نوع TypeScript عند ترجمة كودك، ولا يبقى منه شيء وقت التشغيل. أما مخطط Zod فهو الجزء الذي يبقى: يعمل داخل برنامجك ويتحقق من كل حمولة لحظة وصولها — استجابة API، أو جسم webhook، أو رسالة تُسحب من طابور — قبل أن يعتمد عليها كودك. الصق عيّنة منها بصيغة JSON، فتكتب لك هذه الصفحة المخططات جاهزة للصق في وحدة: كل كائن له مفاتيح يصبح "z.object" مسمّى، وتُدمج عناصر المصفوفة في عنصر واحد، ويأتي بعد كل مخطط نوع TypeScript الذي ينتجه.
هذه هي الإجابة التي تعطيها صفحة JSON إلى TypeScript لمستند JSON نفسه، مكتوبة في صورة أداة تحقق لا في صورة أنواع. فالصفحتان تقرآن استنتاجًا واحدًا، ولذلك فإن أي المفاتيح اختيارية، وأي القيم تصير اتحادًا، وأين يبقى null منفصلًا عن مفتاح مفقود، وما اسم كل كائن متداخل، أمور تُحسم مرة واحدة ولا تفعل الصفحتان سوى كتابتها مرتين. قواعد الدمج هذه هي موضوع الدليل في صفحة JSON إلى TypeScript، ولن تُروى هنا مرة أخرى. أما هذا الدليل فعمّا يفعله المخطط ببيانات حقيقية حين يعمل، وعمّا يتركه لك لتقرره.
ما يمر، وما يُرفض
يقبل المخطط الذي تكتبه هذه الصفحة العيّنة التي كُتب منها، في Zod 3 وفي Zod 4 على السواء، إلا في الحالة الوحيدة التي لها تنبيه خاص بها: رقم لانهائي في Zod 4. وفيما وراء تلك العيّنة يقبل كل ما يبقى داخل حدود ما يقوله المخطط، وهذا ما يعنيه ذلك للبيانات التي ستصلك فعلًا:
- ما دون حد العمق يكون المفتاح المكتوب بلا ".optional()" إلزاميًا. فالحمولة التي تُغفله تُرفض، وكذلك الحمولة التي تضع فيه نوعًا من القيم لا يذكره المخطط — نصًا حيث يقول "z.number()"، أو كائنًا حيث يقول "z.string()".
- المفتاح المكتوب مع ".optional()" يجوز أن يغيب، والمكتوب مع ".nullable()" يجوز أن يحمل null. ولا يمرر أي منهما نوعًا آخر من القيم، فالمفتاح الاختياري إذا حضر فعليه مع ذلك أن يحمل ما يذكره المخطط.
- يقبل "z.union" أي عضو من أعضائه ولا شيء سواه. ويقبل "z.array" أي عدد من العناصر، ولو لم يكن فيها عنصر واحد، ما دام كل عنصر منها يطابق المخطط المكتوب لعناصره.
- حيث لم تُظهر العيّنة شيئًا — عناصر مصفوفة فارغة، أو كائن بلا مفاتيح، أو قيمة متداخلة وراء حد العمق — لا يتحقق المخطط مما يوجد هناك: يقبل أي قيمة، أو أي كائن حيث لم يكن لكائن العيّنة مفاتيح، ويخبرك تنبيه بمكان ذلك.
أما المفتاح الذي لا يذكره المخطط فيُمرَّر ثم يُسقط من النتيجة؛ وذلك هو الإسقاط، وله قسم خاص به. ومفتاح واحد يقف خارج هذا كله. فالمفتاح الذي اسمه "__proto__" يُكتب مفتاحًا محسوبًا، بين قوسين معقوفين، لأنه لو كُتب على نحو عادي لعيّن النموذج الأولي (prototype) للكائن الذي يقع فيه بدل أن يسمّي مفتاحًا — ومع ذلك لا تعيد أي من نسختي Zod ذلك المفتاح فيما يرجعه التحليل، ولا يتحقق Zod 4 من قيمته أصلًا.
لماذا لا يزيد المخطط صرامته من تلقاء نفسه
في كل عيّنة أمور مشتركة لا تستطيع إثباتها: كل معرّف عدد صحيح، وكل بريد إلكتروني على هيئة بريد إلكتروني، ودور لم يحمل قط غير admin. هذا انتظام، والانتظام ليس فحصًا. والمولّد يغريه أن يدوّنه مع ذلك — ".int()" على المعرّفات، و"z.email()" على العناوين، وliteral على الدور — وهذا المولّد لا يفعل ذلك أبدًا، لأن العيّنة تُظهر ما يمكن أن تحمله بياناتك ولا تُظهر أبدًا ما يجب أن تحمله. النوع الأضيق من بياناتك يكلفك خطأ في الترجمة على جهازك أنت. أما المخطط الأضيق من بياناتك فيكلفك طلبًا مرفوضًا في بيئة الإنتاج: أول دور ليس admin، وأول معرّف قيمته 7.5، وأول عنوان لم يتوقعه النمط.
ثم إن الفحوص نفسها كانت ستتبدل من تحتك. إذ يرفض "z.uuid()" في Zod 4 نصوصًا على هيئة UUID كان ".uuid()" في Zod 3 يقبلها، لأنه يفحص بتات variant فيها، ويرفض ".int()" في Zod 4 عددًا صحيحًا خارج النطاق الآمن كان نظيره في Zod 3 يمرّره — فالفحص المخمَّن من عيّنة اليوم سيكون فحصًا مختلفًا بحسب نسخة Zod التي تثبّتها. فلا يُكتب في المخطط إذن شيء تكتفي العيّنة بالإيحاء به. وحيث يكون لذلك وزن عند وصول البيانات، تقوله لك الصفحة في تنبيه بدلًا من ذلك، ويبقى التعديل بيدك.
ما لن يقوله المخطط عن نفسه
تحت المخطط، تسرد الصفحة ما لاحظته ولم تكتبه فيه: تنبيه لكل نوع مما يلي حين ينطبق، يُذكر مرة واحدة مع كل موضع يصدق فيه. ويُكتب الموضع بالطريقة التي تسمّي بها المخرجات الأشياء — "Customer.phone" للمفتاح، و"Root.tags[]" لعناصر مصفوفة، والمفتاح بين علامتي اقتباس وقوسين معقوفين حيث لا يكون معرّفًا (identifier) وفي حالة "__proto__" — كي يمكن العثور عليه في المخطط بنظرة واحدة. والمثال الذي يُحمَّل مع الصفحة يعرض كل الأنواع ما عدا نوع Infinity.
- لا شيء سوى null. لم تحمل العيّنة قط في ذلك الموضع شيئًا غير null، فيقول المخطط "z.null()" ويرفض أول قيمة حقيقية. قرّر ما سيحمله الحقل حين يُملأ واكتب ذلك بنفسك — "z.string().nullable()" مثلًا — أو الصق عيّنة يحمل فيها قيمة.
- Infinity. الرقم الذي يتجاوز ما يستطيع رقم في JavaScript أن يحمله، مثل 1e999، يصير Infinity أو -Infinity عند تحليل JSON. يرفض "z.number()" في Zod 4 الرقم اللانهائي ويقبله نظيره في Zod 3، فهذه في Zod 4 الحالة الوحيدة التي يرفض فيها مخطط العيّنة نفسها التي صُنع منها. والسؤال الذي يطرحه ذلك يخص البيانات لا المخطط: فخانات الرقم تضيع قبل أن تراها أي أداة تحقق، ولذلك فإن السؤال عن كون تلك القيمة رقمًا أصلًا يجيب عنه النظام الذي كتبها.
- أعداد صحيحة. كان كل رقم في ذلك الموضع عددًا صحيحًا، و"z.number()" يقبل 7.5 أيضًا. وحيث يجب أن تكون القيمة عددًا صحيحًا — معرّفًا أو عدّادًا أو كمية — أضف ".int()" بنفسك؛ وحيث تكون سعرًا صادف أن جاء بلا كسور، فاتركها كما هي. ويُحجب التنبيه عن كل موضع يحمل عددًا صحيحًا خارج النطاق الآمن، من "Number.MIN_SAFE_INTEGER" إلى "Number.MAX_SAFE_INTEGER"، لأن ".int()" في Zod 4 يرفض تلك الأعداد، والنصيحة التي تجعل مخططًا يرفض عيّنته نفسها هي النوع الوحيد الذي لن تقدمه الصفحة.
- تعذّر الاستنتاج. عناصر المصفوفة الخالية، والكائن الذي لا مفاتيح له، والقيمة المتداخلة على عمق يزيد على 100 مستوى، كلها لا تعطي الاستنتاج ما يستند إليه، فلا يتحقق المخطط مما يوجد هناك: "z.unknown()" يقبل أي قيمة كانت، و"z.record(z.string(), z.unknown())" يقبل أي كائن. الصق عيّنة تحمل فيها تلك المصفوفة عناصر ويحمل فيها ذلك الكائن مفاتيح، أو اكتب ذلك الجزء من المخطط بيدك.
لا يغيّر التنبيه بايتًا واحدًا من المخطط أبدًا. إنه جملة بجانبه، بلغة الصفحة، والتعديل الذي يشير إليه لك أن تجريه أو أن تتخطاه. ولا يوجد تنبيه عن صيغ النصوص ولا عن literal ولا عن enum: فكل منها سيكون تخمينًا لقاعدة لا تستطيع العيّنة أن تُظهرها.
المفاتيح التي لا يذكرها المخطط تُسقط
يمرر "z.object" الكائن الذي يحمل مفاتيح لا يذكرها، ويتركها خارج ما يعيده. هذا هو السلوك الافتراضي في Zod بنسختيه، وأهدأ ما يفعله مخطط: فالحقل الذي صادف أن غاب عن عيّنتك يختفي من البيانات التي يتلقاها كودك، دون أي خطأ يقول ذلك. ولهذا تذكره الجملة التي تحت كل مخطط في هذه الصفحة.
لا تختار الصفحة عنك بين الصارم والمتساهل، لأن كلًّا منهما يدّعي أكثر مما تستطيع عيّنة أن تُظهره. فالكائن الصارم يرفض أي مفتاح لا يذكره — لا مفاتيح غير هذه — ولا تستطيع أي عيّنة أن تثبت ذلك عن الحمولة التالية. والكائن المتساهل يُبقي المفاتيح الزائدة، ويكتسب نوعه المستنتج توقيع فهرسة (index signature) لها، فيكفّ عن أن يكون إجابة صفحة TypeScript. والإسقاط هو السلوك الوحيد الذي يقبل ما يقبله نوع TypeScript ويظل يستنتج ذلك النوع نفسه — فالقيمة ذات الخصائص الزائدة تحقق الواجهة هي أيضًا. ولتختار غير ذلك، عدّل المخطط بيدك:
- لرفض المفاتيح التي لا يذكرها المخطط، اكتب "z.strictObject" حيث تكتب المخرجات "z.object" في Zod 4، أو ألحق ".strict()" بنهاية "z.object" في Zod 3.
- لإبقائها، اكتب "z.looseObject" في Zod 4، أو ألحق ".passthrough()" في Zod 3. وما زال Zod 4 يشغّل هاتين الطريقتين من Zod 3، ويصفهما بأنهما قديمتان (legacy).
كل كائن في المخرجات مخطط قائم بذاته، فيُتخذ القرار كائنًا كائنًا: جعل الجذر صارمًا لا يغيّر شيئًا في الكائنات المتداخلة داخله، وهذا غالبًا ما تريده حين يكون الغلاف الخارجي وحده هو ما يحق لك أن تتشدد فيه.
اسم واحد لكل مخطط ولنوعه
يأتي بعد كل مخطط نوعه — "export type Customer = z.infer<typeof Customer>" مباشرة بعد "export const Customer" — وهكذا يكتب zod.dev أمثلته هو: اسم واحد للقيمة التي تتحقق من البيانات وللنوع الذي تنتجه، لأن TypeScript تُبقي القيم والأنواع في فضاءات أسماء منفصلة. وذلك النوع هو نفسه الذي تطبعه صفحة JSON إلى TypeScript لمستند JSON نفسه، اسمًا باسم ومفتاحًا بمفتاح، بالمفاتيح الاختيارية نفسها والاتحادات نفسها وnull في المواضع نفسها — إلا في الاستثناءات الواردة أدناه.
يتحقق المستودع من هذا الوعد بدل أن يثق به: تمر مجموعة من العيّنات عبر الصفحتين، ثم عبر مترجم TypeScript مع كل نسخة من Zod، ويُسأل المترجم عن كل اسم: هل النوعان متطابقان، وهل يمكن إسناد كل منهما إلى الآخر. وحيث يجيب بغير ذلك، يكون الموضع في عمق المستند. فوراء حد العمق، حيث يحمل مفتاح "z.unknown()"، يستنتج Zod 3 ذلك المفتاح اختياريًا بينما تجعله صفحة TypeScript إلزاميًا. وفي Zod 4 يعجز المترجم عن نوع مصفوفة متداخلة لعشرات المستويات، فيبلّغ عن الخطأ TS2589 في سطر النوع نفسه؛ أما المخطط الذي فوق ذلك السطر فما زال يعمل وما زال يقبل العيّنة، ولا يضيع إلا النوع المستنتج.
تقرأ المقارنتان المفتاح الاختياري كما تقرؤه TypeScript افتراضيًا. أما مع "exactOptionalPropertyTypes"، المعطَّل ما لم يفعّله المشروع، فإن "z.infer" للمفتاح الاختياري يقبل أيضًا undefined صريحًا، في نسختي Zod كلتيهما، بينما لا يقبله نوع صفحة TypeScript.
مكتوب من أجل Zod 4، وما زال صالحًا في Zod 3
تلتزم المخرجات بما في النسختين الرئيسيتين كلتيهما — "z.object" و"z.array" و"z.union" على عضوين أو أكثر، و"z.string()" و"z.number()" و"z.boolean()" و"z.null()" و"z.unknown()" و"z.record(z.string(), z.unknown())" و".optional()" و".nullable()" و"z.infer" — تحت سطر الاستيراد الذي تبدأ به أمثلة zod.dev نفسه. لا شيء فيها وصل مع Zod 4، ولا شيء فيها موسوم هناك بأنه مهمل، فالمشروع الذي لم ينتقل بعد عن Zod 3 يستطيع أن يلصقها كما هي.
غير أن النص نفسه لا يتصرف تصرفًا واحدًا في النسختين، وكل فرق مذكور في هذا الدليل حيث يهم. يرفض "z.number()" في Zod 4 الرقم اللانهائي بينما يقبله نظيره في Zod 3، وهذا هو تنبيه Infinity. والمفتاح الذي قيمته "z.unknown()" اختياري في النوع المستنتج في Zod 3 وإلزامي في نظيره في Zod 4 — وإلزامي عند تحليل البيانات أيضًا، بدءًا من Zod 4.4 — ومفتاح كهذا لا تكتبه هذه المخرجات إلا وراء حد العمق. ويتحقق Zod 3 من المفتاح الذي اسمه "__proto__" ولا يتحقق منه Zod 4. ويعجز المترجم عن نوع Zod 4 لمصفوفة متداخلة لعشرات المستويات، بينما ينجح في حساب نوع Zod 3.
المخرجات مكتوبة من أجل Zod العادي، بالطرائق: "z.string().nullable().optional()". أما Zod Mini فيكتب المخطط نفسه بالدوال بدلًا من ذلك، "z.optional(z.nullable(z.string()))"، ولذلك لا يستطيع Zod Mini تشغيل المخرجات كما هي.
لماذا يأتي الجذر أخيرًا
تطبع صفحة TypeScript الجذر أولًا والكائنات التي يستخدمها بعده، لأن النوع يجوز استخدامه قبل السطر الذي يصرّح به. أما المخطط فلا يجوز فيه ذلك: إنه قيمة، وقراءة "const" فوق تصريحه تُطلق ReferenceError بينما لا تزال الوحدة قيد التحميل. لذلك يأتي كل مخطط هنا بعد كل مخطط يستخدمه، ويأتي الجذر أخيرًا — Customer وLineItem أولًا في المثال، ثم Root الذي يحملهما — ولهذا فإن الجذر الذي يفتتح صفحة TypeScript يختتم هذه الصفحة.
وترتيب كهذا موجود دائمًا. فالاستنتاج شجرة، وكل كائن يستخرجه يُستخدم من موضع واحد بالضبط، فلا يحتاج أي مخطط إلى الإشارة إلى نفسه ولا إلى مخطط مطبوع بعده، ولا تحتاج المخرجات أبدًا إلى "z.lazy".
الأسئلة الشائعة
- لماذا يخرج المعرّف "z.number()" لا "z.number().int()"؟
- لأن العيّنة تستطيع أن تُظهر أن كل معرّف حتى الآن كان عددًا صحيحًا، لكنها لا تستطيع أن تُظهر أن المعرّف التالي سيكون كذلك. والصفحة تقول ذلك عوضًا عن كتابته: تنبيه الأعداد الصحيحة يسرد كل موضع كان فيه كل رقم عددًا صحيحًا، وحيث تعلم أن قيمة ما يجب أن تبقى عددًا صحيحًا، فإضافة ".int()" تعديل بكلمة واحدة. ويُترك التنبيه حيث يكون الرقم عددًا صحيحًا خارج النطاق الآمن، لأن ".int()" في Zod 4 سيرفض تلك العيّنة نفسها.
- لماذا يخرج عنوان البريد الإلكتروني "z.string()" مجردًا؟
- لأن النص الذي يشبه بريدًا إلكترونيًا في عيّنتك لا يقول شيئًا عن النص التالي، وفحص الصيغة الصحيح لا بد أن يتبع أنماط Zod نفسه، وهي تتغير بين النسخ — إذ يرفض "z.uuid()" في Zod 4 نصوصًا كان ".uuid()" في Zod 3 يمرّرها. وتبقى التواريخ وعناوين URL ومعرّفات UUID نصوصًا للسبب نفسه، والحقل الذي لم يحمل قط سوى قيم قليلة لا يصير أبدًا enum ولا literal. فإن كنت تعرف القاعدة فاكتبها فيه؛ فالمخطط كود عادي تملكه أنت.
- لماذا تفشل عيّنتي أنا في Zod 4؟
- إنها تحمل رقمًا يتجاوز ما يستطيع رقم في JavaScript أن يحمله — 1e999 مثلًا — صار Infinity أو -Infinity عند تحليل JSON، و"z.number()" في Zod 4 يرفض الرقم اللانهائي بينما يقبله نظيره في Zod 3. ويسمّي تنبيه Infinity كل موضع معني. وفيما عدا هذه الحالة، يقبل المخطط دائمًا العيّنة التي جاء منها، لأن كل قيمة في العيّنة دخلت فيه، والمستودع يتحقق من ذلك في النسختين على مجموعة من العيّنات.
- لماذا اختفى حقل أرسلته من نتيجة التحليل؟
- لأن المخطط لا يذكره. يقبل "z.object" الكائن ذا المفاتيح الزائدة ويعيده من دونها، في النسختين كلتيهما، والمفتاح الذي غاب عن عيّنتك مفتاح لم يتعلمه المخطط قط. أضفه إلى المخطط، أو اجعل ذلك الكائن وحده متساهلًا — "z.looseObject" في Zod 4 أو ".passthrough()" في Zod 3 — إن كان على المفاتيح غير المعروفة أن تمر دون أن تُمس.
- نقلت مخططًا إلى ما تحت مخطط آخر فحصلت على ReferenceError. لماذا؟
- لأن المخطط قيمة، وJavaScript لا تسمح بقراءة قيمة فوق السطر الذي يعرّفها. تطبع الصفحة كل مخطط بعد كل مخطط يستخدمه، والجذر أخيرًا، لهذا السبب بالذات؛ أبقِ المخطط فوق كل ما يشير إليه فيزول الخطأ.
- لماذا يُسمّى المخطط Customer لا CustomerSchema؟
- هذا اصطلاح zod.dev نفسه: المخطط والنوع الذي يستنتجه يتقاسمان اسمًا واحدًا، وهو ما تسمح به TypeScript لأن القيم والأنواع تعيش في فضاءات أسماء منفصلة. أما الاسم نفسه فهو الذي تعطيه صفحة JSON إلى TypeScript للكائن نفسه، فهو الكلمة نفسها في الصفحتين، للمخطط ولنوعه على السواء.
- لأي نسخة من Zod كُتبت المخرجات؟
- كُتبت من أجل Zod 4، دون أن تستخدم شيئًا يفتقده Zod 3، ولذلك تعمل دون تغيير على أي منهما. وتختلف النسختان في مواضع قليلة — الرقم اللانهائي، والمفتاح الواقع وراء حد العمق، والمفتاح الذي اسمه "__proto__"، ونوع المصفوفة المتداخلة إلى عمق كبير جدًا — وكل منها مشروح أعلاه. وهي مكتوبة وفق Zod العادي بطرائق متسلسلة؛ أما Zod Mini فيكتب المخطط نفسه بالدوال، ولا يستطيع تشغيلها كما هي.
- هل أستطيع لصق بيانات حقيقية، بما فيها بيانات الاعتماد؟
- نعم. يُستنتج المخطط داخل متصفحك: ما تلصقه يُقرأ على جهازك أنت، ولا يُرسل إلى خادم ولا يُخزَّن ولا يُسجَّل. ثم إن المخطط لا يحمل أي قيمة من قيمك، بل مفاتيحك فقط ونوع القيمة تحت كل منها، فرمز الوصول (token) الموجود في العيّنة يخرج "z.string()" لا أكثر.
أدوات ذات صلة
- JSON إلى TypeScript
المخطط الذي تأخذه من هذه الصفحة يعمل جزءًا من كودك ويتحقق من البيانات في كل مرة تصل فيها. تطبع تلك الصفحة الإجابة نفسها لمستند JSON نفسه في صورة أنواع TypeScript وحدها بالأسماء نفسها، تُفحص عند ترجمة كودك ولا تضيف شيئًا وقت التشغيل.
- JSON إلى Go
تترك لك هذه الصفحة أن تشترط أعدادًا صحيحة حيث كان كل رقم في عيّنتك عددًا صحيحًا. تحدد تلك الصفحة الشكل نفسه بأسماء الأنواع نفسها، لكن ليس في Go نوع هو رقم JSON فحسب، لذا تجعل نوع الحقل عددًا صحيحًا حيث يكون كل رقم مكتوبًا عددًا صحيحًا، والقيمة المكتوبة بنقطة عشرية لن يُفك ترميزها فيه.
- مُختبِر JSONPath
اختبار استعلامات JSONPath (RFC 9535) على JSON.
- مولّد جداول Markdown
أنشئ جداول Markdown من CSV أو TSV أو JSON وحاذِ أعمدتها.