المشكلة: حاجتين بيوصفوا نفس الشكل
عندك interface Order في الـ TypeScript، وorder.json كـ JSON Schema. لو حد زوّد حقل في واحد ونسي التاني:
- الـ type بيقول فيه
discount: والـ schema بترفضه بـadditionalProperties. الـ API بقى بيرفض طلبات سليمة. - أو العكس: الـ schema بتقبل حقل الكود مش متوقعه، فبيتعامل معاه على إنه
undefined.
الحل: مصدر واحد للحقيقة، والتاني يتولّد منه أوتوماتيك. عندك أكتر من طريقة.
JSON Schemaهي الأصل
json-schema-to-typescriptيولّد types
أو TypeBox / Zodكود هو الأصل
يولّد schema + type
الطريقة ١: Ajv بيعمل type guard
typed.ts
import { Ajv2020 } from "ajv/dist/2020.js";import type { JSONSchemaType } from "ajv";interface CreateUser { name: string; age?: number; role: "admin" | "user";}const schema: JSONSchemaType<CreateUser> = { type: "object", properties: { name: { type: "string", minLength: 2 }, age: { type: "integer", nullable: true }, role: { type: "string", enum: ["admin", "user"] }, }, required: ["name", "role"], additionalProperties: false,};const ajv = new Ajv2020();const validate = ajv.compile(schema);const input: unknown = JSON.parse('{"name":"Saeed","role":"admin"}');if (validate(input)) { console.log(input.name.toUpperCase(), input.role); // ← input بقى CreateUser} else { console.log(validate.errors);}- الـ
validate(input): type guard. جوه الـifالـ TypeScript عارف إنinputنوعهCreateUser. الناتج:SAEED admin. - الـ
JSONSchemaType<CreateUser>: بيخلي الـ compiler يراجع إن الـ schema مطابقة للـ interface. جرّبنا نكتبname: { type: "number" }والـtscرفض:The types of 'properties.name' are incompatible. - فخ الـ optional: أي حقل optional لازم يتكتب فيه
nullable: true، وإلاtscبيقولProperty 'nullable' is missing. ده شرط من Ajv نفسه، مش من الـ spec. - الحدود: رسايل الـ TS error طويلة جدًا، والـ unions المعقدة بتتعب. وفيه
JTDSchemaTypeلو بتستخدم JSON Type Definition بدل JSON Schema.
الطريقة ٢: الـ schema هي الأصل ← types
لما الـ schemas JSON ومشتركة مع لغات تانية، ولّد الـ types منها:
gen-types.js
import { compile } from "json-schema-to-typescript";const schema = { title: "User", type: "object", properties: { id: { type: "integer" }, name: { type: "string", description: "اسم اليوزر" }, role: { enum: ["admin", "user"] }, tags: { type: "array", items: { type: "string" } }, }, required: ["id", "name"], additionalProperties: false,};console.log(await compile(schema, "User", { bannerComment: "" }));output
export interface User { id: number; /** * اسم اليوزر */ name: string; role?: "admin" | "user"; tags?: string[];}- الـ
description: بقت JSDoc، فبتظهر في الـ hover. - الـ
integer: بقىnumber، لأن TS مفيهوش integer. - القيود زي
minLengthبتضيع: الـ type مش بيقدر يعبّر عنها، بس الـ validator لسه بيراجعها. - فيه CLI:
json2tsفي نفس الـ package. شغّله في الـ build أو في script، وراجع الـ docs للـ flags.
الطريقة ٣: الـ types هي الأصل ← schema
types.ts
export interface Order { /** @minimum 1 */ id: number; status: "new" | "paid" | "shipped"; /** @format email */ customerEmail: string; note?: string;}terminal
pnpm add -D ts-json-schema-generatorpnpm exec ts-json-schema-generator --path types.ts --type Order- الناتج: schema بـ
"$schema": "http://json-schema.org/draft-07/schema#"وجواهاdefinitions.Orderفيهاrequired: ["id", "status", "customerEmail"]وadditionalProperties: false، والـ@minimumوالـ@formatبقوا keywords. - خلي بالك: الناتج draft-07 بـ
definitions، مش 2020-12. استخدمه معimport Ajv from "ajv". - فيه كمان
typescript-json-schema: أقدم وبنفس الفكرة. - العيب: القيود بتتكتب في JSDoc comments، وده سهل تغلط فيه ومفيش autocomplete.
الطريقة ٤: TypeBox — الكود بيطلّع JSON Schema
الـ TypeBox بيخليك تكتب بـ functions، والناتج JSON Schema حقيقي ومعاه type:
typebox.ts
import Type, { type Static } from "typebox";import Ajv from "ajv/dist/2020.js";import addFormats from "ajv-formats";const User = Type.Object({ name: Type.String({ minLength: 2 }), email: Type.String({ format: "email" }), age: Type.Optional(Type.Integer({ minimum: 0 })), role: Type.Union([Type.Literal("admin"), Type.Literal("user")]),}, { additionalProperties: false });type User = Static<typeof User>; // { name: string; email: string; age?: number; role: "admin" | "user" }console.log(JSON.stringify(User)); // JSON Schema عاديةconst ajv = addFormats(new Ajv({ allErrors: true }));const validate = ajv.compile(User);console.log(validate({ name: "S", email: "x", role: "boss" }), ajv.errorsText(validate.errors));- الـ
Userنفسه object JSON Schema:{"type":"object","required":["name","email","role"],"properties":{...},"additionalProperties":false}، والـ union بقتanyOfبـconst. - الـ
Static<typeof User>: بيطلّع الـ TypeScript type. مصدر واحد. - بيشتغل مع Ajv وFastify على طول: لأن الناتج JSON Schema.
- وفيه validator خاص بيه:
Value.Check(User, data)منtypebox/value. - اسم الـ package: الإصدار الجديد اسمه
typebox(1.x). القديم كان@sinclair/typebox(0.34). الـ API قريب، بس راجع الـ docs لو بتنقل.
الطريقة ٥: Zod 4 ← JSON Schema
لو المشروع أصلًا بـ Zod، Zod 4 بيطلّع JSON Schema built-in:
zod-schema.js
import { z } from "zod";const User = z.object({ name: z.string().min(2), email: z.email(), age: z.number().int().min(0).optional(), role: z.enum(["admin", "user"]).default("user"),});console.log(JSON.stringify(z.toJSONSchema(User), null, 2));output (مختصر)
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "name": { "type": "string", "minLength": 2 }, "email": { "type": "string", "format": "email", "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*..." }, "age": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "role": { "default": "user", "type": "string", "enum": ["admin", "user"] } }, "required": ["name", "email", "role"], "additionalProperties": false}- الـ
roleفيrequired: لأن الـ default إنه بيوصف الـ output (بعد ما الـ default اتحط). لو عايز شكل الـ input اكتبz.toJSONSchema(User, { io: "input" })، وساعتها الـ required بقى["name", "email"]بس. - الـ
int(): بيطلّعmaximum: 9007199254740991(أكبر safe integer). - الـ
target:"draft-7"أو"draft-4"أو"openapi-3.0". الأخير بيطلّعnullable: trueبدلtypearray. - حاجات مينفعش تتحول:
z.date()بيرميDate cannot be represented in JSON Schema، و.transform()بيرميTransforms cannot be represented in JSON Schema. فيه option اسمهunrepresentable: "any"بيحطهم{}. - الـ
.refine()بتضيع بهدوء: refine بيقارنpassبـconfirmطلع schema عادية من غير الشرط. يعني الـ schema الناتجة أضعف من الـ Zod schema. - الـ Zod 3: مفيهوش
toJSONSchema، وكانوا بيستخدموا package اسمهاzod-to-json-schema. - والعكس موجود: Zod 4 فيه
z.fromJSONSchema()، جرّبناه على schema بسيطة واشتغل. راجع الـ docs لأنه جديد.
JSON Schema ولا Zod؟
| السؤال | JSON Schema (+ Ajv / TypeBox) | Zod |
|---|---|---|
| الـ schema نفسها | JSON / data | كود TypeScript |
| تتشارك مع Python / Go | آه، زي ما هي | لازم تحوّلها الأول |
| OpenAPI والـ LLMs والـ editors | native | بعد toJSONSchema |
| شروط custom بكود | صعب (keywords custom في Ajv) | سهل بـ refine |
| تحويل البيانات | محدود (coerce / defaults) | transform وcoerce وpipe |
| السرعة | Ajv من الأسرع (كود متولد) | كويسة، وZod 4 أسرع من 3 |
| حجم الـ bundle للـ frontend | Ajv تقيل نسبيًا | أخف، وفيه zod/mini |
| الـ DX في TS | TypeBox كويس، JSONSchemaType متعب | ممتاز |
- اختار JSON Schema لو: الـ schema contract بين فرق أو لغات، أو أصلها OpenAPI، أو بتستخدمها لـ config files أو MongoDB أو LLM.
- اختار Zod لو: كل حاجة TypeScript، ومحتاج transforms و refinements، وخصوصًا forms على الـ frontend.
- اختار TypeBox لو: عايز JSON Schema حقيقي مع DX شبه Zod، وخصوصًا مع Fastify.
- وينفع تجمع: Zod جوه التطبيق، و
z.toJSONSchemaيطلّع الـ schema للـ OpenAPI أو للـ LLM.
خلّصت الفصل لما
🧠 اختبر نفسك
١) ليه z.toJSONSchema حط الحقل اللي عليه default في required؟
لأنه by default بيوصف الـ output بعد الـ parse، والحقل ساعتها موجود دايمًا. استخدم
io: input لشكل اللي هيتبعت.٢) Zod schema فيها refine إن تاريخ النهاية بعد البداية، وطلّعتها JSON Schema للـ OpenAPI. فيه مشكلة؟
آه، الـ refine بيضيع بهدوء، فالـ schema في الـ docs أضعف من الحقيقة. وثّق الشرط في description، وخلي الـ validation الحقيقي على السيرفر.
٣) عندك services بـ Python وNode بيستخدموا نفس الـ events. تختار إيه؟
JSON Schema كملفات مشتركة، وكل لغة تعمل validation بيها، والـ TS types تتولد منها.