التطبيق · فصل ١٠

الـ TypeScript — مصدر واحد للحقيقة

JSONSchemaType · type guard · json-schema-to-typescript · ts-json-schema-generator · TypeBox · Static · Zod 4 · z.toJSONSchema · single source of truth
🎯 الهدف: تخلي الـ schema والـ TypeScript type ميتفرقوش أبدًا، وتعرف تختار بين JSON Schema وTypeBox وZod.

المشكلة: حاجتين بيوصفوا نفس الشكل

عندك interface Order في الـ TypeScript، وorder.json كـ JSON Schema. لو حد زوّد حقل في واحد ونسي التاني:

  • الـ type بيقول فيه discount: والـ schema بترفضه بـ additionalProperties. الـ API بقى بيرفض طلبات سليمة.
  • أو العكس: الـ schema بتقبل حقل الكود مش متوقعه، فبيتعامل معاه على إنه undefined.

الحل: مصدر واحد للحقيقة، والتاني يتولّد منه أوتوماتيك. عندك أكتر من طريقة.

الطريقة ١: 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 بدل type array.
  • حاجات مينفعش تتحول: 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 والـ editorsnativeبعد toJSONSchema
شروط custom بكودصعب (keywords custom في Ajv)سهل بـ refine
تحويل البياناتمحدود (coerce / defaults)transform وcoerce وpipe
السرعةAjv من الأسرع (كود متولد)كويسة، وZod 4 أسرع من 3
حجم الـ bundle للـ frontendAjv تقيل نسبيًاأخف، وفيه zod/mini
الـ DX في TSTypeBox كويس، 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 تتولد منها.