JSON بهتنهایی فقط شکل نوشتن داده را مشخص میکند؛ یعنی Parser میتواند تشخیص دهد که آکولادها، براکتها، کوتیشنها و مقدارها از نظر Syntax درست هستند یا نه. اما یک سند JSON معتبر میتواند از نظر برنامه کاملاً اشتباه باشد: فیلد اجباری نداشته باشد، نوع مقدار نادرست باشد، عدد از محدوده مجاز خارج شود یا ساختار تودرتو با قرارداد API هماهنگ نباشد. JSON Schema برای حل همین مسئله ساخته شده است؛ زبانی توصیفی که شکل، نوع، محدودیتها و قواعد داده JSON را تعریف میکند.
پاسخ سریع: JSON Schema چیست؟
JSON Schema یک استاندارد برای توصیف و اعتبارسنجی ساختار داده JSON است. با Schema میتوان مشخص کرد مقدار ریشه Object یا Array باشد، چه Propertyهایی مجاز یا اجباریاند، نوع هر فیلد چیست، String چه طول یا الگویی داشته باشد، Number در چه بازهای قرار بگیرد و Array چه نوع اعضایی بپذیرد. سپس یک Validator، داده واقعی را با Schema مقایسه میکند و ناسازگاریها را گزارش میدهد. JSON Schema جای Parser را نمیگیرد؛ ابتدا JSON باید از نظر Syntax قابل Parse باشد و بعد با Schema اعتبارسنجی شود.
چرا JSON معتبر برای یک سیستم کافی نیست؟
فرض کنید API ثبتنام این داده را دریافت کند:
{
"email": 12345,
"age": -8,
"roles": "admin"
}
این متن از نظر Syntax یک JSON معتبر است، اما احتمالاً با قرارداد برنامه سازگار نیست. ایمیل باید String باشد، سن منفی نباشد و roles احتمالاً باید Array باشد. Parser فقط Syntax را میفهمد و نمیتواند درباره این قواعد تصمیم بگیرد. JSON Schema این انتظارها را بهشکل قابلخواندن برای ماشین ثبت میکند.
تفاوت Syntax Validation، Schema Validation و Business Validation
| سطح بررسی | پرسش اصلی | نمونه خطا |
|---|---|---|
| Syntax Validation | آیا متن JSON قابل Parse است؟ | کامای اضافه یا کوتیشن بستهنشده |
| Schema Validation | آیا شکل و نوع داده با قرارداد هماهنگ است؟ | age بهجای Number یک String است |
| Business Validation | آیا داده با قوانین واقعی سیستم سازگار است؟ | کد تخفیف منقضی شده یا موجودی کافی نیست |
Schema میتواند بخش بزرگی از قواعد ساختاری را پوشش دهد، اما جای منطق Domain را نمیگیرد. مثلاً میتواند بگوید تاریخ پایان String با قالب مشخص است، ولی برای تشخیص اینکه تاریخ پایان بعد از تاریخ شروع است معمولاً به منطق برنامه یا قابلیتهای تکمیلی نیاز دارید.
یک JSON Schema ساده چگونه نوشته میشود؟
Schema خود یک سند JSON است. نمونه زیر قراردادی برای یک کاربر تعریف میکند:
{
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer", "minimum": 0 },
"active": { "type": "boolean" }
},
"required": ["name", "age"]
}
فیلد $schema نسخه یا Dialect مورد استفاده را معرفی میکند. type نوع ریشه را تعیین میکند، properties قواعد Propertyها را نگه میدارد و required مشخص میکند کدام کلیدها باید وجود داشته باشند.
وجود یک Property در بخش properties آن را اجباری نمیکند. برای اجباری کردن باید نام آن در آرایه required قرار بگیرد.
Keywordهای اصلی در JSON Schema
| Keyword | کاربرد | نمونه |
|---|---|---|
type | تعیین نوع مقدار | "string" |
properties | تعریف Propertyهای Object | {"name": {...}} |
required | اجباری کردن کلیدها | ["name"] |
items | تعریف ساختار اعضای Array | {"type":"integer"} |
enum | محدود کردن مقدار به گزینههای مشخص | ["draft","published"] |
const | الزام یک مقدار ثابت | "v1" |
minimum | حداقل عدد | 0 |
pattern | الگوی Regex برای String | "^[A-Z]+$" |
additionalProperties | کنترل کلیدهای تعریفنشده | false |
انواع داده قابل استفاده در Schema
JSON Schema با انواع پایه JSON کار میکند:
objectبرای مجموعه Propertyهاarrayبرای فهرست مرتب مقدارهاstringبرای متنnumberبرای عدد صحیح یا اعشاریintegerبرای عدد بدون بخش کسریbooleanبرای true و falsenullبرای مقدار تهی
تفاوت number و integer مهم است. مقدار 5 با هر دو سازگار است، اما 5.5 فقط با number سازگار خواهد بود.
چگونه چند نوع را برای یک فیلد مجاز کنیم؟
گاهی فیلد میتواند String یا null باشد:
{
"type": ["string", "null"]
}
این روش فقط نوع را مشخص میکند. اگر هدف مدلسازی چند ساختار متفاوت باشد، استفاده از anyOf، oneOf یا allOf معمولاً خواناتر و دقیقتر است.
تعریف Object و Propertyهای اجباری
{
"type": "object",
"properties": {
"id": { "type": "integer" },
"title": { "type": "string", "minLength": 1 },
"price": { "type": "number", "minimum": 0 }
},
"required": ["id", "title", "price"],
"additionalProperties": false
}
additionalProperties: false باعث میشود کلیدهای تعریفنشده رد شوند. این گزینه برای قراردادهای سختگیرانه API مفید است، اما در سیستمهایی که باید با نسخههای آینده سازگار بمانند میتواند مشکلساز شود. قبل از بستن کامل Object، سیاست Versioning و توسعه فیلدها را در نظر بگیرید.
محدودیت تعداد Propertyها
با minProperties و maxProperties میتوان تعداد Propertyهای Object را محدود کرد:
{
"type": "object",
"minProperties": 1,
"maxProperties": 10
}
این محدودیت تعداد کل Propertyها را میسنجد و درباره نام یا معنی آنها تصمیم نمیگیرد.
اعتبارسنجی String
برای Stringها میتوان طول، الگو و قالب معنایی تعیین کرد:
{
"type": "string",
"minLength": 3,
"maxLength": 50,
"pattern": "^[a-z0-9-]+$"
}
pattern از Regular Expression استفاده میکند. رفتار جزئی Regex ممکن است به Validator وابسته باشد، بنابراین الگوهای پیچیده را در Runtimeهای هدف آزمایش کنید.
استفاده از format
{
"type": "string",
"format": "email"
}
Keyword format برای قالبهایی مانند email، date-time و uri بهکار میرود، اما میزان سختگیری آن میان Validatorها یکسان نیست. بعضی پیادهسازیها format را فقط Annotation میدانند و برخی آن را بهصورت Assertion فعال میکنند. بنابراین تنظیمات Validator را بررسی کنید و برای قواعد حساس فقط به format تکیه نکنید.
اعتبارسنجی Number و Integer
{
"type": "number",
"minimum": 0,
"maximum": 100,
"multipleOf": 0.5
}
این Schema عددهای بین صفر تا صد را میپذیرد که مضربی از نیم باشند. برای مرزهای غیرشامل از exclusiveMinimum و exclusiveMaximum استفاده میشود.
نمونه محدودیت درصد
{
"type": "number",
"minimum": 0,
"maximum": 100
}
اگر مقدار فقط باید عدد صحیح باشد، type را integer قرار دهید. در غیر این صورت مقدارهایی مانند 12.5 نیز پذیرفته میشوند.
تعریف Array و نوع اعضا
{
"type": "array",
"items": {
"type": "string",
"minLength": 1
},
"minItems": 1,
"maxItems": 5,
"uniqueItems": true
}
این Array باید بین یک تا پنج String غیرخالی داشته باشد و اعضا تکراری نباشند. uniqueItems بر مبنای برابری JSON کار میکند و برای دادههای بزرگ یا Objectهای پیچیده ممکن است هزینه پردازشی بیشتری داشته باشد.
Array شامل Object
{
"type": "array",
"items": {
"type": "object",
"properties": {
"sku": { "type": "string" },
"quantity": { "type": "integer", "minimum": 1 }
},
"required": ["sku", "quantity"],
"additionalProperties": false
}
}
این الگو در سفارشها، سبد خرید و فهرست رکوردهای API رایج است. هر عضو Array مستقل با Schema داخلی بررسی میشود.
Tuple Validation با prefixItems
اگر جایگاه اعضای Array معنی متفاوتی دارد، میتوان برای هر موقعیت Schema جدا تعریف کرد:
{
"type": "array",
"prefixItems": [
{ "type": "number" },
{ "type": "number" },
{ "type": "string" }
],
"items": false
}
این نمونه آرایهای با سه عضو میسازد: دو عدد و سپس یک String. استفاده از Objectهای نامدار اغلب خواناتر است، اما Tuple برای مختصات، رکوردهای فشرده یا قراردادهای ثابت کاربرد دارد.
enum و const چه تفاوتی دارند؟
enum چند مقدار مجاز و const دقیقاً یک مقدار ثابت را تعریف میکند:
{
"properties": {
"status": {
"enum": ["draft", "published", "archived"]
},
"apiVersion": {
"const": "v1"
}
}
}
enum فقط برای String نیست و میتواند Number، Boolean، null یا حتی ساختارهای دیگر را نیز شامل شود، هرچند فهرستهای ساده خواناتر و قابل نگهداریترند.
ترکیب Schemaها با allOf، anyOf و oneOf
| Keyword | شرط اعتبار | کاربرد معمول |
|---|---|---|
allOf | همه Schemaها باید معتبر باشند | ترکیب قواعد مشترک |
anyOf | حداقل یکی معتبر باشد | پذیرش چند شکل ممکن |
oneOf | دقیقاً یکی معتبر باشد | Variantهای انحصاری |
not | Schema داخلی نباید معتبر باشد | ممنوع کردن یک الگو |
مثال oneOf
{
"oneOf": [
{
"type": "object",
"properties": {
"type": { "const": "email" },
"address": { "type": "string", "format": "email" }
},
"required": ["type", "address"]
},
{
"type": "object",
"properties": {
"type": { "const": "sms" },
"phone": { "type": "string" }
},
"required": ["type", "phone"]
}
]
}
وجود Property تشخیصدهنده مانند type کمک میکند Variantها همپوشانی نداشته باشند. بدون آن ممکن است یک داده با چند شاخه همزمان معتبر شود و oneOf شکست بخورد.
استفاده مجدد از قواعد با $defs و $ref
برای جلوگیری از تکرار میتوان Schemaهای کوچک را در $defs تعریف و با $ref استفاده کرد:
{
"$defs": {
"positiveInteger": {
"type": "integer",
"minimum": 1
}
},
"type": "object",
"properties": {
"quantity": { "$ref": "#/$defs/positiveInteger" },
"page": { "$ref": "#/$defs/positiveInteger" }
}
}
این روش نگهداری Schemaهای بزرگ را سادهتر میکند و احتمال اختلاف میان تعریفهای تکراری را کاهش میدهد. برای Referenceهای خارجی باید سیاست دسترسی، Cache و امنیت Validator مشخص باشد؛ در بسیاری از سامانهها بهتر است Schemaهای مورد اعتماد بهصورت محلی Bundle شوند.
قواعد شرطی با if، then و else
{
"type": "object",
"properties": {
"country": { "type": "string" },
"postalCode": { "type": "string" }
},
"if": {
"properties": {
"country": { "const": "IR" }
},
"required": ["country"]
},
"then": {
"properties": {
"postalCode": { "pattern": "^[0-9]{10}$" }
},
"required": ["postalCode"]
}
}
شرطها برای قراردادهای وابسته به مقدار مفیدند، اما Schemaهای بسیار شرطی میتوانند دشوار و مبهم شوند. در قواعد پیچیده، بخشی از Validation را در لایه Business نگه دارید.
وابستگی میان Propertyها
گاهی وجود یک فیلد، فیلد دیگری را اجباری میکند. برای نمونه اگر cardNumber وجود دارد، billingAddress نیز لازم است:
{
"type": "object",
"dependentRequired": {
"cardNumber": ["billingAddress"]
}
}
این قاعده فقط وابستگی وجودی را بررسی میکند. معتبر بودن محتوای billingAddress باید در properties یا Schema جدا تعریف شود.
description، title و default چه نقشی دارند؟
این Keywordها بیشتر Annotation هستند:
titleنام خوانا برای Schema یا فیلدdescriptionتوضیح هدف و محدودیتdefaultمقدار پیشنهادی یا پیشفرض مستنداتیexamplesنمونههای قابل فهم برای انسان و ابزار
وجود default معمولاً به این معنی نیست که Validator خودکار مقدار را وارد میکند. بعضی کتابخانهها چنین قابلیت افزودهای دارند، اما رفتار استاندارد Validation صرفاً بررسی داده است. تغییر داده باید آگاهانه و مستند انجام شود.
نمونه کامل Schema برای محصول
{
"$id": "product.schema.json",
"title": "Product",
"type": "object",
"properties": {
"id": {
"type": "integer",
"minimum": 1
},
"name": {
"type": "string",
"minLength": 2,
"maxLength": 120
},
"price": {
"type": "number",
"minimum": 0
},
"currency": {
"enum": ["IRR", "USD", "EUR"]
},
"tags": {
"type": "array",
"items": { "type": "string", "minLength": 1 },
"uniqueItems": true,
"maxItems": 20
},
"dimensions": {
"type": "object",
"properties": {
"width": { "type": "number", "exclusiveMinimum": 0 },
"height": { "type": "number", "exclusiveMinimum": 0 },
"unit": { "enum": ["mm", "cm", "m"] }
},
"required": ["width", "height", "unit"],
"additionalProperties": false
}
},
"required": ["id", "name", "price", "currency"],
"additionalProperties": false
}
این Schema نوع، Propertyهای اجباری، بازه عددی، enum، Array یکتا و Object تودرتو را ترکیب میکند. برای استفاده واقعی باید سیاست null، نسخه قرارداد، مقدارهای پولی و دقت Number نیز روشن باشد.
نمونه داده معتبر و نامعتبر
داده معتبر
{
"id": 42,
"name": "Mechanical Keyboard",
"price": 89.5,
"currency": "USD",
"tags": ["keyboard", "usb"],
"dimensions": {
"width": 35.4,
"height": 12.6,
"unit": "cm"
}
}
داده نامعتبر
{
"id": 0,
"name": "K",
"price": -10,
"currency": "GBP",
"tags": ["keyboard", "keyboard"],
"stock": 5
}
در نمونه دوم id از حداقل کمتر است، name کوتاه است، price منفی است، currency در enum نیست، tags عضو تکراری دارد و stock بهدلیل additionalProperties: false مجاز نیست.
اعتبارسنجی JSON Schema در JavaScript
کتابخانههای مختلفی وجود دارند و API دقیق آنها به نسخه وابسته است. الگوی کلی چنین است:
const validate = compileSchema(schema);
const valid = validate(data);
if (!valid) {
console.log(validate.errors);
}
Schema باید یکبار Compile و برای درخواستهای متعدد Reuse شود. Compile کردن Schema در هر Request هزینه اضافی ایجاد میکند. همچنین نسخه Draft مورد پشتیبانی کتابخانه را با Dialect Schema هماهنگ کنید.
اعتبارسنجی در Python
from jsonschema import Draft202012Validator
validator = Draft202012Validator(schema)
errors = sorted(validator.iter_errors(data), key=lambda e: list(e.path))
for error in errors:
print(list(error.path), error.message)
جمعآوری همه خطاها برای فرمها و ابزارهای توسعه مفید است، اما در ورودیهای بزرگ ممکن است هزینه بیشتری داشته باشد. برای مسیرهای حساس میتوان سیاست توقف در اولین خطا یا محدودیت تعداد خطاها تعریف کرد.
اعتبارسنجی در C#
در اکوسیستم .NET چند کتابخانه با سطح پشتیبانی متفاوت از Draftها وجود دارد. ساختار کلی معمولاً شامل Parse کردن Schema، Evaluate کردن داده و خواندن Result است:
var schema = LoadSchema(schemaText);
var result = schema.Evaluate(jsonData);
if (!result.IsValid)
{
Console.WriteLine("JSON does not match the schema.");
}
نام کلاسها و گزینهها به کتابخانه انتخابی وابستهاند. پیش از استفاده، پشتیبانی از Draft 2020-12، Referenceها، format و خروجی خطا را بررسی کنید.
پیام خطای خوب چه اطلاعاتی دارد؟
- مسیر دقیق داده، مانند
items[2].price - Keyword شکستخورده، مانند minimum یا required
- مقدار مورد انتظار و در صورت امن بودن مقدار واقعی
- شناسه یا مسیر Schema مربوط
- پیام قابل فهم برای کاربر و جزئیات جداگانه برای توسعهدهنده
در محیط Production اطلاعات حساس را داخل Log یا پاسخ API تکرار نکنید. گاهی مقدار نامعتبر Password، Token یا داده شخصی است و نباید در پیام خطا چاپ شود.
خطاهای رایج هنگام طراحی JSON Schema
فراموش کردن required
تعریف فیلد در properties آن را اجباری نمیکند. این اشتباه باعث میشود Object خالی نیز معتبر باشد.
استفاده بیشازحد از additionalProperties: false
این گزینه قرارداد را بسته و دقیق میکند، اما Forward Compatibility را کاهش میدهد. برای API عمومی، اضافه شدن فیلد جدید ممکن است Clientهای قدیمی را بشکند.
اتکا به format بدون بررسی Validator
برخی Validatorها format را فقط Annotation تلقی میکنند. برای داده حساس، رفتار واقعی کتابخانه را تست کنید.
استفاده اشتباه از oneOf
اگر بیش از یک شاخه همزمان معتبر باشد، oneOf شکست میخورد. با const یا Property تشخیصدهنده شاخهها را از هم جدا کنید.
مدل کردن قواعد تجاری پیچیده در Schema
Schema باید قرارداد داده را روشن کند، نه اینکه تمام منطق سیستم را در خود جای دهد. پیچیدگی زیاد، تست و نگهداری را دشوار میکند.
Versioning در JSON Schema
Schema نیز مانند API تغییر میکند. تغییرهای سازگار شامل افزودن Property اختیاری یا گسترش enum در شرایط مناسب است؛ تغییرهای ناسازگار شامل اجباری کردن فیلد جدید، تغییر Type یا محدود کردن Range میشود. برای مدیریت نسخهها:
- هر نسخه Schema را با شناسه روشن نگه دارید.
- تغییرهای Breaking را در نسخه جدید قرارداد منتشر کنید.
- نمونه داده و تست سازگاری برای نسخههای قبلی داشته باشید.
- Producer و Consumer را جداگانه تست کنید.
- نسخه Draft خود JSON Schema را نیز ثبت کنید.
امنیت JSON Schema
Schema Validation یک مرز امنیتی کامل نیست، اما میتواند سطح حمله را کاهش دهد. محدود کردن طول String، تعداد اعضای Array، عمق ساختار و کلیدهای اضافی از ورودیهای غیرقابل کنترل جلوگیری میکند. بااینحال باید به این موارد توجه کرد:
- Referenceهای خارجی میتوانند درخواست شبکه یا دسترسی ناخواسته ایجاد کنند؛ Resolution را محدود کنید.
- Regexهای پیچیده ممکن است زمان پردازش زیادی مصرف کنند.
- Schema یا داده بسیار بزرگ میتواند حافظه و CPU را درگیر کند.
- اعتبارسنجی جای Sanitization، Authorization یا Encoding خروجی را نمیگیرد.
- Schemaهای دریافتشده از کاربر نباید بدون محدودیت Compile و اجرا شوند.
نکات عملکرد برای Schemaهای بزرگ
- Schema را یکبار Compile و Cache کنید.
- Referenceها را پیشاپیش Resolve یا Bundle کنید.
- حداکثر اندازه، عمق و تعداد خطاها را محدود کنید.
- برای Payloadهای بزرگ، فقط در صورت پشتیبانی از Streaming Validation استفاده کنید.
- Schemaهای تکراری را با $defs و $ref ساده کنید، اما زنجیره Reference را بیشازحد عمیق نکنید.
- عملکرد را با دادههای واقعی و بدترین حالت اندازهگیری کنید.
بهترین روشهای طراحی JSON Schema
- از Schema کوچک شروع کنید و محدودیتها را بر اساس قرارداد واقعی اضافه کنید.
- نوع ریشه را همیشه صریح بنویسید.
- required را آگاهانه تعریف کنید و با properties اشتباه نگیرید.
- برای Range، Length و تعداد اعضا حد منطقی تعیین کنید.
- از title، description و examples برای مستندسازی استفاده کنید.
- Schema را همراه نمونههای معتبر و نامعتبر تست کنید.
- نسخه Draft و پشتیبانی Validator را ثابت و مستند نگه دارید.
- قواعد Business را از قواعد ساختاری جدا کنید.
چکلیست اعتبارسنجی Schema
- آیا خود Schema از نظر JSON Syntax معتبر است؟
- آیا Draft مورد استفاده با Validator هماهنگ است؟
- آیا type ریشه مشخص شده است؟
- آیا فیلدهای لازم در required قرار گرفتهاند؟
- آیا null در جاهایی که ممکن است دریافت شود مدل شده است؟
- آیا additionalProperties مطابق سیاست Versioning تنظیم شده است؟
- آیا enumها و Rangeها با Domain واقعی هماهنگاند؟
- آیا نمونههای مرزی، خالی، null و بسیار بزرگ تست شدهاند؟
- آیا پیام خطا مسیر فیلد را نشان میدهد؟
- آیا Reference و Regex از نظر امنیت و عملکرد کنترل شدهاند؟
JSON Schema چه زمانی انتخاب مناسبی است؟
وقتی چند سرویس، تیم یا زبان برنامهنویسی باید روی قرارداد داده مشترک توافق کنند، JSON Schema بسیار مفید است. همچنین برای API، فایل تنظیمات، فرمهای پویا، تولید مستندات، تست داده و کنترل ورودی Pipeline کاربرد دارد. اگر قرارداد بسیار ساده و فقط داخل یک برنامه کوچک استفاده میشود، Validation نوعی زبان ممکن است کافی باشد؛ اما در ارتباط میان سیستمها، Schema یک قرارداد مستقل و قابل آزمایش فراهم میکند.
جمعبندی
JSON Schema فاصله میان «JSON قابل Parse» و «داده سازگار با قرارداد» را پر میکند. با type، properties، required، items، enum، محدودیتهای String و Number و ترکیب Schemaها میتوان ساختار داده را دقیق تعریف کرد. طراحی خوب Schema فقط سختگیری بیشتر نیست؛ باید با Versioning، عملکرد، امنیت و منطق واقعی سیستم متعادل باشد. Schema را با نمونههای معتبر و نامعتبر تست کنید، Draft را ثابت نگه دارید و Validation ساختاری را از قواعد تجاری و کنترلهای امنیتی جدا بدانید.