التطبيق · فصل ٨

الـ validation بجد — Ajv من جوه

Ajv · compile once · allErrors · strict · coerceTypes · removeAdditional · useDefaults · errors · ajv-errors · ajv-i18n · standalone
🎯 الهدف: تشغّل Ajv صح في production: instance واحد، compile مرة واحدة، options مفهومة، وerrors نضيفة لليوزر.

Ajv بيشتغل إزاي؟

الـ Ajv (Another JSON Validator) هو أشهر وأسرع validator في JavaScript. السر في السرعة إنه بيحوّل الـ schema لـ كود JavaScript مرة واحدة، وبعدين كل validation مجرد function call.

  • الـ compile غالي: بيحلل الـ schema ويولّد كود ويعمله new Function.
  • الـ validate رخيص جدًا: مجرد ifs متولدة.
  • يبقى القاعدة: instance واحد للتطبيق كله، وcompile مرة واحدة وقت الـ startup.

الغلطة المشهورة: compile جوه الـ handler

bad-handler.js
app.post("/users", (req, res) => {  const validate = new Ajv().compile({ type: "object" /* ... */ }); // ❌ كل request  if (!validate(req.body)) return res.status(400).json(validate.errors);
...
});

قسنا الفرق على schema صغيرة (object فيه حقلين):

الطريقةالوقت لكل request
ajv.compile(schema جديدة) جوه الـ handlerحوالي 0.9ms
validate متعملها compile مرة واحدةأقل من ميكروثانية
  • وفيه مشكلة أكبر من السرعة: Ajv بيخزّن كل schema اتعملها compile في cache جوه الـ instance. لو كل request بيعمل object schema جديد، الـ cache بيكبر. جرّبنا 500 compile لـ object جديد بنفس المحتوى والـ cache وصل 508 entry. ده memory leak.
  • ولو نفس الـ object: Ajv بيرجّع نفس الـ function من الـ cache (f1 === f2 طلعت true).
  • الحل: اعمل compile في أول الملف أو استخدم addSchema + getSchema(id).

الـ options المهمة

ajv.js
import Ajv from "ajv/dist/2020.js";import addFormats from "ajv-formats";export const ajv = new Ajv({  allErrors: true,       // اجمع كل الأخطاء مش أول واحد بس  strict: true,          // الـ default: ارمي error على keywords غلط  coerceTypes: false,    // الـ default: متحوّلش types  useDefaults: false,    // الـ default: متحطش الـ default  removeAdditional: false, // الـ default: متمسحش حقول});addFormats(ajv);
allErrors

الـ default false: بيقف عند أول غلط. true بيجمع كله، وده أحسن لليوزر في الـ forms. بس خلي بالك منه مع الـ input الضخم أو الـ regex الخطيرة (فصل ١٢).

strict

الـ default true: بيرمي error على keyword مش معروف (minLenght)، أو format مش متسجل، أو حاجات ملخبطة. وفيه أجزاء منه (strictTypes، strictTuples) بتطبع warning بس. سيبه شغال.

coerceTypes

true بيحوّل "2" لـ 2 و"true" لـ true جوه الـ data نفسها. مفيد للـ query string والـ env vars بس. و"array" كمان بيحوّل قيمة واحدة لـ array.

useDefaults

true بيحط قيمة الـ default في الحقول الناقصة جوه الـ object نفسه. يعني بيغيّر الـ input بتاعك (mutation).

removeAdditional

true: بيمسح الحقول الزيادة بس في الأماكن اللي فيها additionalProperties: false. "all": بيمسحها في كل حتة. "failing": بيمسح اللي فشل في الـ additionalProperties schema.

حاجات تانية

validateFormats: false يقفل الـ formats. multipleOfPrecision لمشكلة الكسور. discriminator: true للـ unions. $data: true عشان keyword ياخد قيمته من حقل تاني. راجع الـ docs.

مثال: query string

options.js
import Ajv from "ajv/dist/2020.js";const schema = {  type: "object",  properties: {    page: { type: "integer", minimum: 1, default: 1 },    active: { type: "boolean" },    sort: { enum: ["new", "price"], default: "new" },  },  additionalProperties: false,};// زي query string: كل القيم stringsconst query = { page: "2", active: "true", debug: "1" };const ajv = new Ajv({ coerceTypes: true, useDefaults: true, removeAdditional: true });const validate = ajv.compile(schema);console.log(validate(query), query);console.log(validate({ page: "abc" }), validate.errors[0].instancePath, validate.errors[0].message);
output
true { page: 2, active: true, sort: 'new' }false /page must be integer
  • الـ page وactive: اتحولوا لـ number وboolean.
  • الـ sort: اتحط من الـ default.
  • الـ debug: اتمسح بهدوء بدل ما يفشّل الـ request.
  • الـ "abc": مينفعش يبقى integer، فبرضه فشل. الـ coercion مش بيقبل أي حاجة.

شكل الـ errors

كل error فيه:

الحقلمثالمعناه
instancePath/items/0/qtyمكان الغلط في الـ data (JSON Pointer)، وفاضي للـ root
schemaPath#/properties/items/items/properties/qty/typeمكان القاعدة في الـ schema
keywordtypeأنهي keyword فشل
params{ "type": "integer" }تفاصيل: missingProperty، additionalProperty، limit، allowedValues…
messagemust be integerرسالة بالإنجليزي
  • الـ required بيقول المكان غلط شوية: الـ instancePath بيبقى الـ parent، واسم الحقل في params.missingProperty. نفس الكلام في additionalProperties مع params.additionalProperty. هتعالج ده في الـ Lab.
  • الـ ajv.errorsText(validate.errors): بيجمعهم في سطر واحد، زي data/id must be integer, data/name must NOT have fewer than 2 characters. كويس للـ logs.
  • الـ validate.errors بيتمسح مع كل call: لو هتحتاجه بعدين، خد نسخة قبل ما تنادي validate تاني.
  • متبعتش schemaPath لليوزر في API عام: بيكشف تفاصيل الـ schema الداخلية من غير فايدة.

رسايل بالعربي

i18n.js
import Ajv from "ajv/dist/2020.js";import ajvErrors from "ajv-errors";import localize from "ajv-i18n";const ajv = new Ajv({ allErrors: true });ajvErrors(ajv); // بيضيف keyword اسمه errorMessageconst validate = ajv.compile({  type: "object",  properties: {    phone: {      type: "string",      pattern: "^01[0125][0-9]{8}$",      errorMessage: "رقم الموبايل لازم يبقى 11 رقم ويبدأ بـ 010 أو 011 أو 012 أو 015",    },    age: { type: "integer", minimum: 18 },  },  required: ["phone"],});validate({ phone: "123", age: 10 });// ترجم رسايل Ajv العادية بس، وسيب رسايلك زي ما هيconst builtIn = validate.errors.filter((e) => e.keyword !== "errorMessage");localize.ar(builtIn);for (const e of validate.errors) console.log(e.instancePath, e.message);
output
/phone رقم الموبايل لازم يبقى 11 رقم ويبدأ بـ 010 أو 011 أو 012 أو 015/age  يجب أن يكون >= 18
  • الـ ajv-errors: بيخليك تكتب رسالتك جوه الـ schema بـ errorMessage. لازم allErrors: true.
  • الـ ajv-i18n: بيترجم رسايل Ajv العادية لـ لغات كتير منها ar، وبيعدّل الـ array نفسها.
  • الفخ: لو شغّلت localize.ar على كل الـ errors، هيترجم رسالتك انت كمان لجملة عامة. عشان كده فلترنا.
  • أو ابني الرسالة بنفسك: من keyword وparams. ده أحسن لو الـ frontend هو اللي بيترجم.

Standalone: من غير Ajv وقت التشغيل

تقدر تولّد كود الـ validation وقت الـ build، وتشغّله من غير ما تعمل compile وقت الـ startup:

build-validators.js
import { writeFileSync } from "node:fs";import Ajv from "ajv/dist/2020.js";import standaloneCode from "ajv/dist/standalone/index.js";const ajv = new Ajv({ code: { source: true } });const validate = ajv.compile({  type: "object",  properties: { name: { type: "string", minLength: 2 } },  required: ["name"],});writeFileSync("validate-user.cjs", standaloneCode(ajv, validate));
use.js
import validate from "./validate-user.cjs";console.log(validate({ name: "Saeed" }), validate({}), validate.errors?.[0].message);// true false must have required property 'name'
  • الفايدة: startup أسرع (serverless، edge)، ومفيش new Function وقت التشغيل، وده مهم لو عندك Content Security Policy بتمنع eval.
  • الملف لسه محتاج ajv في الـ dependencies: بيعمل require لـ functions صغيرة زي ucs2length.
  • جرّبنا esm: true: الملف طلع فيه require جوه ESM ورمى require is not defined in ES module scope. فطلّعناه .cjs، وده اشتغل. راجع الـ docs لأن ده ممكن يتصلح.

لغات تانية

نفس الـ schema JSON تشتغل في أي لغة، وده أهم ميزة. مثال بـ Python:

validate.py
from jsonschema import Draft202012Validatorschema = {    "type": "object",    "properties": {"name": {"type": "string", "minLength": 2}, "age": {"type": "integer"}},    "required": ["name"],}v = Draft202012Validator(schema)for err in v.iter_errors({"name": "S", "age": "30"}):    print(list(err.path), err.message)
output
['name'] 'S' is too short['age'] '30' is not of type 'integer'
  • Python: jsonschema (اتجرّبت بـ 4.26)، وفيه fastjsonschema لو محتاج سرعة.
  • Go: santhosh-tekuri/jsonschema.
  • Java: networknt/json-schema-validator.
  • .NET: JsonSchema.Net.
  • Rust: الـ crate اسمها jsonschema.
  • PHP: opis/json-schema.
  • عشان تختار: موقع json-schema.org فيه صفحة tools ونتايج test suite رسمية بتوضح كل validator بيدعم إيه. راجع الـ docs.

خلّصت الفصل لما

🧠 اختبر نفسك
١) الـ memory بتاعة السيرفر بتزيد مع الوقت والـ validation في الكود كده: ajv.compile({...}) جوه الـ handler. ليه؟
كل request بيعمل schema object جديد، وAjv بيخزّن كل compile في الـ cache بتاعه. اعمل compile مرة واحدة بره الـ handler.
٢) ينفع coerceTypes: true على JSON body؟
يشتغل، بس غالبًا غلط. الـ JSON عنده types حقيقية، فلو جه "30" ده bug عند الـ client. استخدمه للـ query string والـ env vars.
٣) الـ required error فيه instancePath فاضي. إزاي أعرف الحقل الناقص؟
من params.missingProperty.
٤) ليه ممكن تستخدم standalone code؟
startup أسرع ومفيش new Function وقت التشغيل، فمناسب للـ serverless وللـ CSP اللي بتمنع eval.