مدیریت JSON برای پیکربندی، fixture و داده محلی
جمعبندی: پیکربندی باید لایهبندی و اعتبارسنجی شود؛ fixture باید قطعی، خوانا و قابل diff باشد. اگر این دو نوع فایل بد مدیریت شوند، هزینه عیبیابی مدت طولانی بالا میماند.

جمعبندی: پیکربندی را بهصورت پیشفرض، محیط و بازنویسی محلی با اعتبارسنجی هنگام راهاندازی بسازید پیش از آنکه نگران قالب فایل باشید؛ fixture را در زمان، شناسه، ترتیب کلید و تورفتگی قطعی کنید تا قابل diff بماند، و تنها بعد خودکارسازی ماسک را بر عهده بگیرید.
این دو نوع JSON در منطق کسبوکار شرکت نمیکنند، اما تعیین میکنند «همکار جدید چقدر طول میکشد تا راه بیفتد» و «مکانیابی یک مشکل تولیدی چقدر طول میکشد».
۱. پیکربندی: ساختار سهلایه
| لایه | منبع | نگهداریکننده | کامیت میشود |
|---|---|---|---|
| پیشفرضها | ثابتهای کد / default.json |
توسعهدهندهها | بله |
| پیکربندی محیط | config/production.json، متغیرهای محیطی |
عملیات | بله (بدون راز) |
| بازنویسی محلی | .env.local، config/local.json |
افراد | نه (در gitignore) |
ترتیب ادغام: پیشفرضها → محیط → محلی، با بازنویسی هر لایه بعدی بر لایه قبلی. فقط به عمقی که کسبوکار لازم دارد ادغام کنید؛ کورکورانه ادغام عمیق نکنید — ادغام عمیق مشکلاتی مانند «آرایهها بهطور غیرمنتظره الحاق شدند» را فوقالعاده سخت قابل تشخیص میکند.
/**
* Merges configuration layers by priority; later layers override earlier ones, and arrays are replaced wholesale rather than concatenated.
* @param layers Configuration layers from lowest to highest priority
* @returns The merged configuration object
*/
function mergeConfigLayers(layers: Array<Record<string, unknown>>): Record<string, unknown> {
return layers.reduce<Record<string, unknown>>((mergedConfig, currentLayer) => {
for (const [configKey, configValue] of Object.entries(currentLayer)) {
const isPlainObject =
typeof configValue === 'object' && configValue !== null && !Array.isArray(configValue);
const existingValue = mergedConfig[configKey];
if (isPlainObject && typeof existingValue === 'object' && existingValue !== null) {
mergedConfig[configKey] = mergeConfigLayers([
existingValue as Record<string, unknown>,
configValue as Record<string, unknown>,
]);
continue;
}
// Arrays and scalars are always replaced wholesale to avoid accidental element concatenation
mergedConfig[configKey] = configValue;
}
return mergedConfig;
}, {});
}
۲. هنگام راهاندازی اعتبارسنجی کنید، تا زمان اجرا صبر نکنید
هزینه خطای پیکربندی بهصورت نمایی با تأخیر در کشفش رشد میکند:
| چه زمانی کشف میشود | هزینه |
|---|---|
| هنگام راهاندازی | یک خط لاگ، restart و ادامه |
| در نخستین درخواست | برخی ویژگیها خطا میدهند، یک ساعت عیبیابی |
| وقتی کار زمانبندیشده اجرا شود | هشدار نیمهشب |
| در روز تسویه | حادثه تطبیق حساب |
import Ajv2020 from 'ajv/dist/2020';
import configSchema from './config.schema.json' with { type: 'json' };
const ajv = new Ajv2020({ allErrors: true, strict: true });
/**
* Validates the configuration and throws immediately on failure, implementing fail fast.
* @param rawConfig The raw merged configuration
* @returns The validated configuration
*/
export function assertConfig(rawConfig: unknown): Record<string, unknown> {
const validate = ajv.compile(configSchema);
if (!validate(rawConfig)) {
const messages = (validate.errors ?? []).map(
(errorItem) => `${errorItem.instancePath || '/'} ${errorItem.message ?? ''}`,
);
throw new Error(`配置校验失败:\n${messages.join('\n')}`);
}
return rawConfig as Record<string, unknown>;
}
اصول پشتیبان:
- وقتی مورد الزامی گم است فوراً خارج شوید؛ پیشفرض «بهنظر منطقی» ندهید (مانند پیشفرض گذاشتن آدرس پایگاهداده روی
localhost)؛ - روی خطای نوع فوراً خارج شوید؛ رشته
"false"بولیfalseنیست، پس متغیرهای محیطی باید پس از خواندن صریح تبدیل شوند؛ - رازها هرگز به مخزن نمیروند؛ فقط از متغیرهای محیطی یا سرویس مدیریت راز بخوانید.
۳. برای فایلهای پیکربندی از کدام قالب استفاده کنیم
| فایل | قالب | دلیل |
|---|---|---|
package.json |
JSON استاندارد | مدیر بسته فقط این را میپذیرد |
tsconfig.json |
JSONC | کامنت مجاز، رسماً پشتیبانیشده |
.vscode/settings.json |
JSONC | همان بالا |
| پیکربندی کسبوکار | YAML یا JSONC | توضیح معنای هر آیتم لازم دارد |
| فهرست متغیرهای محیطی | .env |
جفتهای کلید-مقدار مسطح، تزریق آسان |
یک آزمون عملی: آیا این فایل توسط ابزارهای غیرانسانی خوانده میشود؟ اگر بله → JSON استاندارد؛ اگر فقط کد خودتان میخواند → گویش اشکالی ندارد.
۴. fixtureهای آزمون: پوشهها و نامگذاری
tests/fixtures/
├─ valid/
│ ├─ order-minimal.json
│ ├─ order-full.json
│ └─ order-with-null-fields.json
├─ invalid/
│ ├─ order-missing-id.json
│ ├─ order-price-as-number.json
│ └─ order-nested-broken.json
└─ edge/
├─ big-integer.json
├─ emoji-and-surrogate.json
└─ deep-nesting-64.json
قرارداد نامگذاری خوب object-scenario است: order-missing-field بسیار مفیدتر از test1.json است.
نام فایلهای غیر ASCII گهگاه در زنجیره ابزارهای چندسکویی مشکل رمزگذاری ایجاد میکنند، پس نام فایلها را انگلیسی نگه دارید در حالی که محتوا میتواند چینی بماند.
۴.۱ سه نوع کیس، سه نوع انتظار
| پوشه | انتظار | هدف |
|---|---|---|
valid/ |
اعتبارسنجی پاس میشود | جریان اصلی، تولید نوع |
invalid/ |
اعتبارسنجی شکست میخورد و خطا به فیلد مورد انتظار اشاره میکند | مدیریت خطا، متن پیام |
edge/ |
رفتار صریح است (پاس یا شکست اشکالی ندارد، اما باید اعلان شود) | محافظت در برابر رگرسیون |
JSONTestSuite از پیشوندهای y_ / n_ / i_ برای همان سه نوع استفاده میکند (باید بپذیرد / باید رد کند / به پیادهسازی وابسته)،
و میتوانید مستقیم آن قرارداد نامگذاری را قرض بگیرید.
۵. قطعیت: fixture را قابل diff کنید
fixtureهای ناپایدار بازبینی کد را بیفایده میکنند:
| منبع نویز | چگونه برخورد کنیم |
|---|---|
| زمان جاری | به یک ثابت قفلش کنید، مثلاً 2026-01-01T00:00:00Z |
| شناسههای تصادفی / UUID | از دنباله افزایشی یا مقادیر ثابت استفاده کنید |
| ترتیب کلید | هنگام تولید به ترتیب الفبایی مرتب کنید |
| قالببندی شناور | یکسان دو رقم اعشار نگه دارید یا به رشته تبدیل کنید |
| منطقه زمانی | همهجا از UTC استفاده کنید |
| تورفتگی | ثابت ۲ فاصله، در CI توسط Prettier بررسی شود |
/**
* Recursively sorts keys to produce stable, diffable JSON text.
* @param value The value to serialize
* @param indentWidth Indentation width
* @returns A JSON string with a stable order
*/
function toStableJson(value: unknown, indentWidth = 2): string {
/**
* Recursively sorts object keys.
* @param input Any JSON value
* @returns A new value with sorted keys
*/
const sortKeysDeep = (input: unknown): unknown => {
if (Array.isArray(input)) {
return input.map((item) => sortKeysDeep(item));
}
if (typeof input === 'object' && input !== null) {
return Object.fromEntries(
Object.keys(input)
.sort()
.map((entryKey) => [entryKey, sortKeysDeep((input as Record<string, unknown>)[entryKey])]),
);
}
return input;
};
return JSON.stringify(sortKeysDeep(value), null, indentWidth);
}
۶. ماسک و انطباق
داده واقعی نمیتواند مستقیم بهعنوان fixture استفاده شود:
- نامها → جایگذارندههایی مانند «John Doe» و «Jane Roe»؛
- شماره تلفن → شمارههای آشکارا ساختگی مانند
13800138000؛ - شماره شناسایی → شمارههای جعلی با قالب درست اما رقم کنترل نامعتبر؛
- آدرس → ساختار استان/شهر را نگه دارید، شماره پلاک را بیندازید؛
- مبلغها → مرتبه بزرگی را نگه دارید، اما هرگز اطلاعات حساب واقعی را نگه ندارید.
بهتر است تیم یک scripts/mask-fixture.mjs داشته باشد که روی نمونه تولیدی صادرشده اجرا شود و نسخه ماسکشده تولید کند،
بهجای «سه جا را دستی عوض کن، بقیه را فراموش کن».
۷. مهاجرت و نسخهها
fixtureها هم کهنه میشوند. فیلد schemaVersion به fixture اضافه کنید، یا فایل VERSION در پوشه بگذارید:
{
"schemaVersion": 3,
"id": "1001",
"amount": "89.50"
}
وقتی قرارداد API ارتقا مییابد:
- پوشه fixture جدید اضافه کنید (
v3/)؛ fixtureهای قدیمی را در جای خود تغییر ندهید؛ - fixtureهای قدیمی را برای رگرسیون نگه دارید تا نسخه قدیمی API بازنشسته شود؛
- هر دو نسخه را در CI اجرا کنید تا سازگاری رو به عقب تأیید شود.
۸. چکلیست
- [ ] ساختار سهلایه پیکربندی روشن است و بازنویسیهای محلی کامیت نمیشوند؛
- [ ] اعتبارسنجی هنگام راهاندازی رخ میدهد و شکست فوراً خارج میشود؛
- [ ] رازها فقط از متغیرهای محیطی میآیند؛ فایلهای پیکربندی فقط جایگذارنده دارند؛
- [ ] fixtureها به پوشههای
valid/invalid/edgeتقسیم میشوند و نامها سناریو را بیان میکنند؛ - [ ] fixtureها قطعی (زمان، شناسه و ترتیب ثابت) و قابل diff هستند؛
- [ ] داده واقعی از اسکریپت ماسک عبور کرده است؛
- [ ] ارتقای قرارداد پوشه نسخه جدید اضافه میکند نه ویرایش درجا.
پرسشهای متداول
لایههای پیکربندی چگونه مرتب و ادغام شوند؟
از سه لایه — پیشفرضها، پیکربندی محیط و بازنویسی محلی — استفاده کنید، با ادغام پیشفرض → محیط → محلی و بازنویسی هر لایه بعدی بر قبلی. فقط به عمقی که کسبوکار لازم دارد ادغام کنید و آرایهها و اسکالرها را یکجا جایگزین کنید، تا ادغام عمیق نتواند آرایهها را غیرمنتظره الحاق کند. فایل بازنویسی محلی را در gitignore بگذارید نه اینکه تنظیمات شخصی را کامیت کنید.
چرا پیکربندی را هنگام راهاندازی اعتبارسنجی کنیم؟
هزینه خطای پیکربندی بهصورت نمایی با تأخیر در کشفش رشد میکند: یک خط لاگ هنگام راهاندازی در برابر حادثه تطبیق حساب در روز تسویه. هنگام راهاندازی در برابر JSON Schema اعتبارسنجی کنید و روی موارد الزامی گمشده یا خطاهای نوع فوراً خارج شوید، نه اینکه پیشفرض «بهنظر منطقی» بدهید.
آیا باید داده تولیدی را مستقیم بهعنوان fixture آزمون استفاده کنم؟
نه. نامها، شماره تلفن، شماره شناسایی، آدرس و مبلغها همه به جایگذارنده یا مقادیر آشکارا ساختگی نیاز دارند، و تیم باید scripts/mask-fixture.mjs داشته باشد که روی خروجی تولیدی اجرا شود و نسخه ماسکشده تولید کند، تا از ویرایش دستی که چیزی را جا میاندازد پرهیز شود.