الأربعة الأساسيين
allOf = و (AND)
لازم يطابق كل الـ schemas. بنستخدمها عشان نركّب: “user أساسي + حقول admin”.
anyOf = أو (OR)
لازم يطابق واحدة على الأقل. أسهل وأرخص من oneOf، وغالبًا هي اللي محتاجها.
oneOf = واحدة بالظبط (XOR)
لازم يطابق واحدة بس. لو طابق اتنين، بيفشل. مفيد للـ unions اللي فروعها مش بتتقاطع.
not = لأ
لازم ميطابقش الـ schema. مثلًا username مش root ولا admin. الرسالة must NOT be valid.
compose.json
{ "type": "object", "properties": { "username": { "type": "string", "not": { "enum": ["root", "admin"] } }, "contact": { "anyOf": [ { "type": "string", "format": "email" }, { "type": "string", "pattern": "^01[0125][0-9]{8}$" } ] } }}- الـ username: أي string ما عدا
rootوadmin. - الـ contact: إيميل أو رقم موبايل مصري.
- الـ errors مع
anyOf: Ajv بيرجّع أخطاء كل الفروع ومعاهاmust match a schema in anyOf، فالرسالة ممكن تبقى طويلة ومحيّرة لليوزر.
فخ الـ oneOf: الفروع اللي بتتقاطع
oneof-trap.json
{ "oneOf": [{ "type": "number" }, { "type": "integer" }] }- جرّبنا
5: طلع invalid! لأن5number و integer في نفس الوقت، يعني طابق فرعين. - و
5.5: طلع valid، لأنه طابقnumberبس. - نفس الكلام مع objects:
oneOfبين{required: [card]}و{required: [iban]}يرفض object فيه الاتنين. ساعات ده اللي عايزه، وساعات لأ. - القاعدة: استخدم
anyOfإلا لو محتاج فعلًا “واحدة بس”. والـoneOfلازم يجرّب كل الفروع عشان يتأكد إن مفيش اتنين، فهو أبطأ.
الـ discriminated unions
أشهر استخدام: object شكله بيتغير حسب حقل اسمه type أو kind أو method:
union.js
import Ajv from "ajv/dist/2020.js";const ajv = new Ajv({ allErrors: true, discriminator: true });const validate = ajv.compile({ type: "object", discriminator: { propertyName: "method" }, required: ["method"], oneOf: [ { properties: { method: { const: "card" }, cardNumber: { type: "string", pattern: "^[0-9]{16}$" }, }, required: ["cardNumber"], }, { properties: { method: { const: "wallet" }, phone: { type: "string", pattern: "^01[0125][0-9]{8}$" }, }, required: ["phone"], }, { properties: { method: { const: "cod" } } }, ],});const show = (d) => console.log(JSON.stringify(d), validate(d), validate.errors?.map((e) => `${e.instancePath} ${e.message}`) ?? "");show({ method: "card", cardNumber: "4111111111111111" });show({ method: "card", cardNumber: "41" });show({ method: "bitcoin" });show({ method: "cod" });output
{"method":"card","cardNumber":"4111111111111111"} true{"method":"card","cardNumber":"41"} false [ '/cardNumber must match pattern "^[0-9]{16}$"' ]{"method":"bitcoin"} false [ ' value of tag "method" must be in oneOf' ]{"method":"cod"} true- الـ
constفي كل فرع: هو اللي بيخلي الفروع مش بتتقاطع، فالـoneOfآمن. - الـ
discriminator: keyword جاي من OpenAPI، مش من JSON Schema نفسها. Ajv بيدعمه بـdiscriminator: true. - الفايدة: Ajv بيبص على
methodالأول ويروح للفرع الصح على طول، فالـ error بقى سطر واحد مفهوم. - من غير
discriminator: نفس الـ body بيرجّع ٥ أخطاء من كل الفروع:
output (من غير discriminator)
/cardNumber must match pattern "^[0-9]{16}$"(root) must have required property 'phone'/method must be equal to constant/method must be equal to constant(root) must match exactly one schema in oneOfالـ if / then / else
لما شرط على قيمة حقل بيغيّر قواعد حقل تاني:
shipping.js
import Ajv from "ajv/dist/2020.js";const ajv = new Ajv({ allErrors: true });const validate = ajv.compile({ type: "object", properties: { country: { type: "string" }, postalCode: { type: "string" }, }, required: ["country"], if: { properties: { country: { const: "EG" } }, required: ["country"], }, then: { properties: { postalCode: { type: "string", pattern: "^[0-9]{5}$" } }, required: ["postalCode"], }, else: { properties: { postalCode: { type: "string", minLength: 3 } }, },});for (const data of [{ country: "EG" }, { country: "EG", postalCode: "12" }, { country: "US" }]) { console.log(JSON.stringify(data), validate(data), validate.errors?.map((e) => `${e.instancePath} ${e.message}`).join(" | ") ?? "");}output
{"country":"EG"} false must have required property 'postalCode' | must match "then" schema{"country":"EG","postalCode":"12"} false /postalCode must match pattern "^[0-9]{5}$" | must match "then" schema{"country":"US"} true- الـ
if: schema عادية، نتيجتها مش بتفشّل حاجة. بتحدد بس هنروحthenولاelse. - الـ
then/else: الاتنين اختياريين. - أكتر من شرط: حط كل
if/thenجوهallOfمنفصل:"allOf": [ {if, then}, {if, then} ].
فخ additionalProperties مع allOf
عايز تعمل “schema أساسية” وتوسّعها:
extend-wrong.json
{ "type": "object", "allOf": [ { "type": "object", "properties": { "id": { "type": "integer" } }, "required": ["id"] } ], "properties": { "name": { "type": "string" } }, "additionalProperties": false}- جرّبنا
{"id": 1, "name": "x"}: طلع invalid برسالةmust NOT have additional properties! - ليه؟ الـ
additionalPropertiesبيبص على الـpropertiesاللي جنبه في نفس الـ object بس. هو مش شايفidاللي جوه الـallOf، فبيعتبره زيادة. - الحل في 2019-09 وما بعده:
unevaluatedProperties: falseبدلadditionalProperties: false:
extend-right.json
{ "type": "object", "allOf": [ { "type": "object", "properties": { "id": { "type": "integer" } }, "required": ["id"] } ], "properties": { "name": { "type": "string" } }, "unevaluatedProperties": false}- الـ
unevaluatedProperties: بيشوف كل حقل اتراجع في أي subschema (allOf، $ref، if/then، oneOf…)، والباقي بس هو اللي “زيادة”. - النتيجة:
{"id": 1, "name": "x"}بقى valid، و{"id": 1, "name": "x", "hack": true}اترفض بـmust NOT have unevaluated properties. - ونفس الفكرة للـ arrays:
unevaluatedItems. - في draft-07 مفيش الـ keyword ده: الحل هناك إنك تكرر الـ properties كلها في المكان اللي فيه
additionalProperties.
خلّصت الفصل لما
🧠 اختبر نفسك
١) oneOf بين string بـ maxLength 10 وstring بـ pattern أرقام بس. القيمة 12345؟
invalid، لأنها طابقت الفرعين. استخدم
anyOf.٢) if فيها properties.kind.const = business من غير required. الـ {} هيروح then ولا else؟
then. لأن
properties مش بتراجع حقل مش موجود، فالـ if بقت true.٣) عملت base schema في allOf وحطيت additionalProperties false بره. حقول الـ base بقت مرفوضة. ليه وإزاي تحلها؟
لأن additionalProperties بيشوف الـ properties اللي جنبه بس. استخدم
unevaluatedProperties: false (2019-09 وما بعده).