JSON Schema: از مبانی تا تولید
جمعبندی: کلیدواژههای زیادی وجود دارد، اما فقط حدود یک دوجین روزمره استفاده میشوند. type/properties/required/$ref را درست بگیرید، پیام خطا را خوب مدیریت کنید و هشتاد درصد موارد واقعی را پوشش دادهاید.

جمعبندی: اول $schema را به 2020-12 سنجاق کنید، سپس ساختارهای مشترک را با $defs + $ref استخراج کنید؛ oneOf را فقط وقتی استفاده کنید که دقیقاً یک شاخه باید تطبیق یابد، و هر جای دیگر discriminator یا anyOf را ترجیح دهید.
schema ضمیمه مستندات نیست؛ داراییای است که میتوان اجرا، آزمون و تولیدش کرد.
۱. اول نسخه را روشن کنید
| نسخه | مقدار $schema |
وضعیت | یادداشت |
|---|---|---|---|
| draft-04 | http://json-schema.org/draft-04/schema# |
قدیمی | پروژههای قدیمی و برخی ابزارهای OpenAPI 2.0 |
| draft-07 | http://json-schema.org/draft-07/schema# |
پرکاربرد | پایه عملی OpenAPI 3.0 |
| 2019-09 | https://json-schema.org/draft/2019-09/schema |
موجود | $defs و واژگان حاشیهنویسی را معرفی کرد |
| 2020-12 | https://json-schema.org/draft/2020-12/schema |
توصیهشده کنونی | prefixItems را معرفی کرد و معناشناسی items را تغییر داد |
برای پروژههای جدید از 2020-12 استفاده کنید. توجه کنید در 2020-12 items فقط «عناصر باقیمانده» را مقید میکند — اعلانهای tuple به prefixItems رفتند.
این رایجترین تله هنگام مهاجرت از draft-07 است.
۲. یک schema حداقلی قابل استفاده
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bbdrug.com/schemas/user.json",
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string", "minLength": 1 },
"name": { "type": "string", "minLength": 1, "maxLength": 64 },
"age": { "type": "integer", "minimum": 0, "maximum": 150 },
"tags": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
},
"additionalProperties": false
}
این schema چهار دغدغه رایج را پوشش میدهد: قید نوع، فیلدهای الزامی، محدودیت بازه و قید عناصر آرایه.
۳. نقشه کلیدواژهها
۳.۱ عمومی
| کلیدواژه | هدف | حواستان باشد |
|---|---|---|
type |
نوع؛ میتواند آرایه باشد | "type": ["string", "null"] یعنی nullable |
enum |
مقادیر شمارشی | مقادیر باید یکتا باشند |
const |
قید تکمقداری | برای مقادیر ثابت مانند شماره نسخه خوب است |
$ref |
به schema دیگری ارجاع میدهد | به کل schema ارجاع میدهد و با کلیدواژههای همسایه روی شیء ارجاعدهنده ادغام نمیشود |
۳.۲ عددها
multipleOf، maximum، exclusiveMaximum، minimum، exclusiveMinimum.
توجه کنید در draft-04 exclusiveMaximum یک بولی بود (همراه maximum)؛ فقط از draft-07 به بعد کلیدواژه عددی مستقل شد —
که باعث میشود بسیاری «schemaهای کپیشده از مستندات قدیمی» در حالت سختگیر شکست بخورند.
۳.۳ رشتهها
minLength، maxLength، pattern (یک عبارت باقاعده ECMA-262) و format (یک حاشیهنویسی).
minLength نقطههای کد یونیکد را میشمارد، نه واحدهای UTF-16 و نه بایت. اعتبارسنجی با string.length جاوااسکریپت با آن اختلاف پیدا میکند.
۳.۴ آرایهها
| کلیدواژه | هدف |
|---|---|
prefixItems |
قید tuple سبک 2020-12، تطبیق عناصر یکبهیک با اندیس |
items |
قید یکنواخت روی عناصر باقیمانده (2020-12) |
contains + minContains/maxContains |
دستکم/حداکثر چند عنصر تطبیقیابنده باید حاضر باشد |
uniqueItems |
عناصر باید همه متفاوت باشند (مقایسه عمیق؛ روی آرایههای بزرگ پرهیز کنید) |
minItems / maxItems |
قید طول |
۳.۵ اشیا
properties، required، patternProperties، additionalProperties، propertyNames،
minProperties، maxProperties، dependentRequired، dependentSchemas.
سه باور غلط پرتکرار:
requiredفقط وجود کلید را میخواهد:{"name": ""}معتبر است. برای رد رشته خالی بایدminLength: 1اضافه کنید؛additionalProperties: falseمشکلات سازگاری را بزرگ میکند: یک فیلد اضافه از یک کلاینت کل payload را میشکند، پس تکامل API همه فراخوانها را مجبور به ارتقای همگام میکند؛propertiesکلیدهای اعلامنشده را محدود نمیکند — بهصورت پیشفرض مجازند.
۳.۶ ترکیب منطقی
{
"oneOf": [
{ "type": "object", "required": ["cardNumber"], "properties": { "cardNumber": { "type": "string" } } },
{ "type": "object", "required": ["accountId"], "properties": { "accountId": { "type": "string" } } }
]
}
| کلیدواژه | معناشناسی | شکست معمول |
|---|---|---|
allOf |
همه باید برآورده شوند | چند شاخه قیدهای متناقض روی یک فیلد میگذارند |
anyOf |
دستکم یکی باید برآورده شود | پیام خطا سخت به شاخه مشخص قابل ردیابی است |
oneOf |
دقیقاً یکی باید برآورده شود | داده دو شاخه را تطبیق میدهد و شکست میخورد، هرچند «واضحاً درست» به نظر میرسد |
not |
نباید برآورده شود | کم استفاده، و غیردوستانهترین پیامهای خطا |
oneOf کلیدواژهای است که بیشترین احتمال تولید «داده معتبری که در اعتبارسنجی شکست میخورد» را دارد. اگر قیدهای شاخه همپوشانی دارند (مثلاً هر دو فیلد اضافه را مجاز میدانند)،
یک رکورد چند شاخه را تطبیق میدهد و oneOf کلاً شکست میخورد. در آن حالت به anyOf بروید، یا از فیلد discriminator با const داخل شاخهها استفاده کنید.
{
"type": "object",
"required": ["kind"],
"properties": { "kind": { "enum": ["text", "image"] } },
"if": { "properties": { "kind": { "const": "image" } } },
"then": { "required": ["url", "width", "height"] },
"else": { "required": ["content"] }
}
سبک discriminator از oneOf خواناتر است و خطاهای بهتری میدهد.
۴. تقسیم و بازاستفاده
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bbdrug.com/schemas/order.json",
"$defs": {
"money": { "type": "string", "pattern": "^-?\\d+\\.\\d{2}$" },
"address": {
"type": "object",
"required": ["city"],
"properties": { "city": { "type": "string" }, "detail": { "type": "string" } }
}
},
"type": "object",
"required": ["total", "shipTo"],
"properties": {
"total": { "$ref": "#/$defs/money" },
"shipTo": { "$ref": "#/$defs/address" }
}
}
$defsشکل معرفیشده در 2019-09 است؛ draft-07 ازdefinitionsاستفاده میکند؛- ارجاع بینفایلی از
$idنسبی استفاده میکند، اما در تولید به واکشی schemaهای راه دور در زمان اجرا تکیه نکنید — آنها را بهصورت فایل ایستا بستهبندی کنید؛ - نمیتوانید کنار یک ارجاع قید اضافه کنید (رفتار
$refبا کلیدواژههای همسایه بین نسخهها متفاوت است). وقتی ترکیب لازم دارید ازallOfاستفاده کنید.
۵. اعتبارسنجی زمان اجرا با Ajv
import Ajv2020 from 'ajv/dist/2020';
import addFormats from 'ajv-formats';
import orderSchema from './order.json' with { type: 'json' };
const ajv = new Ajv2020({
allErrors: true,
strict: true,
allowUnionTypes: true,
});
addFormats(ajv);
const validateOrder = ajv.compile(orderSchema);
export function assertOrder(input: unknown): void {
if (validateOrder(input)) {
return;
}
const detail = (validateOrder.errors ?? []).map((errorItem) => ({
path: errorItem.instancePath || '/',
keyword: errorItem.keyword,
message: errorItem.message,
params: errorItem.params,
}));
throw new Error(`Order data failed validation: ${JSON.stringify(detail)}`);
}
نکات کلیدی:
compileرا فقط یک بار صدا بزنید، در سطح بالای ماژول؛ کامپایل دوباره در هر درخواست اعتبارسنجی ۱۰ میلیثانیهای را به صدها میلیثانیه تبدیل میکند؛strict: trueکلیدواژههای غلطنویس را زود آشکار میکند (مثلاًminlengthبهجایminLength)، پس ارزش فعال کردن دارد؛- فقط
ajv-formatsباعث میشودformat: "email"وformat: "date-time"واقعاً اثر کنند؛ - 2020-12 نقطه ورود
ajv/dist/2020میخواهد — از export پیشفرض استفاده نکنید.
۶. طراحی پیامهای خطا
چیزی که اعتبارسنج میدهد «کدام کلیدواژه شکست خورد» است؛ چیزی که کاربر میخواهد «کدام جعبه ورودی اشتباه است». ارزش دارد یک لایه نگاشت اضافه کنید:
const FIELD_LABEL: Record<string, string> = {
'/name': 'Name',
'/age': 'Age',
'/tags': 'Tags',
};
/**
* Convert Ajv errors into display-ready messages.
* @param errors The list of Ajv errors
* @returns An array of display messages
*/
function toMessages(errors: Array<{ instancePath: string; keyword: string; params: unknown }>): string[] {
return errors.map((errorItem) => {
const label = FIELD_LABEL[errorItem.instancePath] ?? errorItem.instancePath;
if (errorItem.keyword === 'required') {
const missing = (errorItem.params as { missingProperty: string }).missingProperty;
return `${label} is missing the required field ${missing}`;
}
if (errorItem.keyword === 'minLength') {
return `${label} is too short`;
}
return `${label} failed validation (${errorItem.keyword})`;
});
}
در سمت API، ساختار ماشینخوان نگه دارید تا فرانتاند تصمیم بگیرد چگونه ارائه دهد:
{
"type": "https://bbdrug.com/problems/validation",
"title": "Request data validation failed",
"status": 422,
"errors": [
{ "path": "/items/0/price", "keyword": "pattern", "message": "Must be a monetary string with two decimal places" }
]
}
۷. عملکرد و تلهها
- هزینه اعتبارسنجی با پیچیدگی schema و اندازه داده رشد میکند؛ چند شاخه
oneOfانفجار ترکیبیاتی میسازند که با هزاران رکورد بسیار محسوس میشود؛ uniqueItemsمقایسه عمیق O(n²) روی آرایه انجام میدهد؛ برای آرایههای بزرگ در لایه کسبوکار یکتا کنید؛- از الگوهای backtracking فاجعهبار در
patternپرهیز کنید (مانند(a+)+$)؛ این نقطه ورود کلاسیک ReDoS است؛ - وقتی
additionalProperties: falseباallOfترکیب میشود، فیلدهای اعلامشده در زیرschema توسط والد «اضافه» داوری میشوند، پس باید در یک schema ادغام شوند یا ازunevaluatedPropertiesاستفاده کنید.
۸. چکلیست
- [ ] 2020-12 سنجاق شده و
$schemaدر بالای فایل اعلام شده است؛ - [ ] ساختارهای مشترک با
$defs+$refاستخراج شدهاند نه کپی-چسباندن؛ - [ ] discriminator بر
oneOfترجیح داده میشود؛ - [ ] نتیجه
compileاعتبارسنج بازاستفاده میشود و حالت سختگیر فعال است؛ - [ ] روشن مستند شده که آیا
formatاعمال میشود؛ - [ ] پاسخهای خطا مسیر، کلیدواژه و پیام کاربر-facing میدهند؛
- [ ] schema در CI است: موارد JSON Schema Test Suite بهعنوان آزمون رگرسیون اجرا میشوند.
پرسشهای متداول
چگونه یک draft JSON Schema انتخاب کنم؟
برای پروژههای جدید از 2020-12 استفاده کنید و $schema را در بالای فایل اعلام کنید. اگر schema به OpenAPI 3.1 میرود، باید 2020-12 هم باشد، چون 3.1 مستقیماً همان نسخه را جاسازی میکند؛ draft-07 را فقط برای پروژههایی که به زنجیره ابزار قدیمیتر بستهاند نگه دارید. این دو برای items و کلیدواژههای همسایه $ref رفتار متفاوت دارند، پس در یک مجموعه schema مخلوطشان نکنید.
چرا additionalProperties: false مشکلات سازگاری را بزرگ میکند؟
هر کلید اعلامنشده را خطا میداند، پس یک فیلد اضافه از یک کلاینت کل payload را میشکند. آنگاه هر فراخوان باید با هر تکامل API همگام ارتقا یابد، وگرنه کلاینتهای قدیمی رد میشوند. schemaهای سبک پیکربندی باید فیلدهای اضافه را صریح ببندند، در حالی که APIهای عمومی باید هزینه سازگاری را بسنجند.
از oneOf استفاده کنم یا anyOf؟
oneOf را فقط وقتی استفاده کنید که داده باید دقیقاً یک شاخه را تطبیق دهد. اگر قیدهای شاخه میتوانند همزمان برقرار باشند، یک رکورد چند شاخه را تطبیق میدهد و oneOf کلاً شکست میخورد؛ به anyOf بروید، یا یک discriminator const داخل هر شاخه اضافه کنید.