Ajv بيشتغل إزاي؟
الـ Ajv (Another JSON Validator) هو أشهر وأسرع validator في JavaScript. السر في السرعة إنه بيحوّل الـ schema لـ كود JavaScript مرة واحدة، وبعدين كل validation مجرد function call.
- الـ compile غالي: بيحلل الـ schema ويولّد كود ويعمله
new Function. - الـ validate رخيص جدًا: مجرد
ifs متولدة. - يبقى القاعدة: instance واحد للتطبيق كله، وcompile مرة واحدة وقت الـ startup.
الغلطة المشهورة: compile جوه الـ handler
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 وصل
508entry. ده memory leak. - ولو نفس الـ object: Ajv بيرجّع نفس الـ function من الـ cache (
f1 === f2طلعتtrue). - الحل: اعمل compile في أول الملف أو استخدم
addSchema+getSchema(id).
الـ options المهمة
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);الـ default false: بيقف عند أول غلط. true بيجمع كله، وده أحسن لليوزر في الـ forms. بس خلي بالك منه مع الـ input الضخم أو الـ regex الخطيرة (فصل ١٢).
الـ default true: بيرمي error على keyword مش معروف (minLenght)، أو format مش متسجل، أو حاجات ملخبطة. وفيه أجزاء منه (strictTypes، strictTuples) بتطبع warning بس. سيبه شغال.
true بيحوّل "2" لـ 2 و"true" لـ true جوه الـ data نفسها. مفيد للـ query string والـ env vars بس. و"array" كمان بيحوّل قيمة واحدة لـ array.
true بيحط قيمة الـ default في الحقول الناقصة جوه الـ object نفسه. يعني بيغيّر الـ input بتاعك (mutation).
true: بيمسح الحقول الزيادة بس في الأماكن اللي فيها additionalProperties: false. "all": بيمسحها في كل حتة. "failing": بيمسح اللي فشل في الـ additionalProperties schema.
validateFormats: false يقفل الـ formats. multipleOfPrecision لمشكلة الكسور. discriminator: true للـ unions. $data: true عشان keyword ياخد قيمته من حقل تاني. راجع الـ docs.
مثال: query string
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);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 |
keyword | type | أنهي keyword فشل |
params | { "type": "integer" } | تفاصيل: missingProperty، additionalProperty، limit، allowedValues… |
message | must 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 الداخلية من غير فايدة.
رسايل بالعربي
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);/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:
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));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:
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)['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. ليه؟
٢) ينفع coerceTypes: true على JSON body؟
"30" ده bug عند الـ client. استخدمه للـ query string والـ env vars.٣) الـ required error فيه instancePath فاضي. إزاي أعرف الحقل الناقص؟
params.missingProperty.٤) ليه ممكن تستخدم standalone code؟
new Function وقت التشغيل، فمناسب للـ serverless وللـ CSP اللي بتمنع eval.