JSON Toolkit

JSON Schema: از مبانی تا تولید

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

متوسطحدود 14 دقیقه مطالعهانتشار 2026-10-30به‌روزرسانی 2026-10-30
تصویر جلد JSON Schema: از مبانی تا تولید

جمع‌بندی: اول $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.

سه باور غلط پرتکرار:

  1. required فقط وجود کلید را می‌خواهد: {"name": ""} معتبر است. برای رد رشته خالی باید minLength: 1 اضافه کنید؛
  2. additionalProperties: false مشکلات سازگاری را بزرگ می‌کند: یک فیلد اضافه از یک کلاینت کل payload را می‌شکند، پس تکامل API همه فراخوان‌ها را مجبور به ارتقای هم‌گام می‌کند؛
  3. 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 داخل هر شاخه اضافه کنید.

منابع

مقالات مرتبط

هم‌گام‌سازی دوطرفه بین Schema و نوع‌های TypeScript

نوع‌ها و schemaها هر یک یک بازه را پوشش می‌دهند: یکی زمان کامپایل، یکی زمان اجرا. پرسش این نیست کدام انتخاب شود، بلکه چگونه این دو خودکار هم‌گام بمانند.

متوسطحدود 11 دقیقه مطالعهبه‌روزرسانی 2026-10-30

نکات کلیدی طراحی قرارداد داده API در JSON

شش چیز پس از انتشار بسیار سخت تغییر می‌کنند: پاکت پاسخ، قالب خطا، شکل صفحه‌بندی، نمایش شناسه و زمان، معناشناسی null در برابر غایب، و تکامل نسخه.

متوسطحدود 13 دقیقه مطالعهبه‌روزرسانی 2026-10-30

بدون additionalProperties، اعتبارسنجی اسکیما هیچ‌چیز را اعتبارسنجی نمی‌کند

قبولی اعتبارسنجی به معنای داده تمیز نیست. Pydantic به‌طور پیش‌فرض فیلدهای ناشناخته را نادیده می‌گیرد، Zod آن‌ها را حذف می‌کند و Ajv رد می‌کند — پارامترهای توهمی و کلیدهای غلط املایی بی‌صدا عبور می‌کنند. این مقاله سه شکل پذیرش بی‌صدا را نشان می‌دهد و دستور کامل می‌دهد: بستن اشیا، کنترل تبدیل نوع، سفت‌کردن هر سطح تودرتو.

Intermediateحدود 8 دقیقه مطالعهبه‌روزرسانی 2026-11-02