دادههای تختشده در بسیاری از خروجیهای تحلیلی، فایلهای CSV، فرمها و سیستمهای ثبت رویداد دیده میشوند. در این دادهها، مسیر هر مقدار داخل نام کلید ذخیره میشود؛ برای مثال user.profile.name بهجای یک Object تودرتو استفاده میشود. JSON Unflatten فرایندی است که این مسیرها را میخواند و دوباره Objectها و Arrayهای اصلی را میسازد. این فرایند در ظاهر ساده است، اما بدون قرارداد روشن برای جداکننده، Array، کلیدهای Escapeشده، مقدارهای خالی و برخورد مسیرها میتواند دادهای متفاوت از ساختار اولیه تولید کند.
Quick Answer: JSON Unflatten مجموعهای از کلیدهای مسیرمحور را به ساختار تودرتوی Object و Array تبدیل میکند. برای بازسازی دقیق باید فرمت مسیر، نحوه تشخیص ایندکس Array، قانون Escape کلیدها و رفتار هنگام برخورد مسیرهایی مانندuserوuser.nameمشخص باشد. بهترین معیار صحت نیز تست Round Trip است: داده را Flatten کنید، دوباره Unflatten کنید و نتیجه را با ساختار اصلی مقایسه کنید.
JSON Unflatten چیست؟
JSON Unflatten یا بازسازی JSON تخت، عملیات معکوس Flatten است. ورودی معمولاً یک Object تکسطحی است که کلیدهای آن مسیرهای منطقی را نشان میدهند و خروجی یک ساختار درختی شامل Objectها، Arrayها و مقدارهای نهایی است. برای نمونه، کلیدهای order.customer.name و order.total باید زیر یک Object به نام order قرار گیرند.
{
"user.id": 42,
"user.profile.name": "Sara",
"user.profile.active": true
}
پس از Unflatten، ساختار مورد انتظار چنین است:
{
"user": {
"id": 42,
"profile": {
"name": "Sara",
"active": true
}
}
}
Unflatten فقط افزودن آکولاد نیست. ابزار باید مسیرها را بخشبندی کند، Container مناسب بسازد، نوع مقدارهای نهایی را حفظ کند و درباره تعارضها تصمیم بگیرد. اگر خروجی قرار است دوباره توسط برنامه مصرف شود، این تصمیمها باید پایدار و قابل پیشبینی باشند.
تفاوت JSON Unflatten با Parse و Formatter
| عملیات | ورودی | خروجی | هدف اصلی |
|---|---|---|---|
| JSON Parse | متن JSON | داده قابل استفاده در برنامه | تبدیل Serialization به ساختار حافظه |
| JSON Formatter | JSON فشرده یا نامرتب | همان ساختار با فاصلهگذاری خوانا | بهبود نمایش بدون تغییر مدل داده |
| JSON Unflatten | کلیدهای مسیرمحور | Object و Array تودرتو | بازسازی سلسلهمراتب داده |
بنابراین Formatter نمیتواند کلید profile.name را خودکار به Object تبدیل کند و Parse نیز درباره معنای نقطه داخل نام کلید تصمیمی نمیگیرد. Unflatten یک تبدیل ساختاری است و باید با قرارداد تولیدکننده داده هماهنگ باشد.
JSON Unflatten چگونه کار میکند؟
الگوریتم معمول، هر کلید تخت را به قطعههای مسیر تقسیم میکند. سپس از ریشه حرکت کرده و برای هر قطعه میانی، Object یا Array لازم را میسازد. مقدار ورودی در آخرین قطعه قرار میگیرد. اگر مسیر customer.address.city باشد، دو Container میانی برای customer و address ایجاد میشود و مقدار در city ثبت میشود.
- یک Object یا Array ریشه ایجاد میشود.
- هر کلید براساس قرارداد مسیر Tokenize میشود.
- برای هر Token میانی، Container مناسب ساخته یا بازیابی میشود.
- نوع Token بعدی بررسی میشود تا Object یا Array بودن مشخص شود.
- مقدار نهایی بدون تبدیل ناخواسته نوع ثبت میشود.
- تعارضها طبق سیاست مشخص گزارش یا حل میشوند.
چرا ترتیب پردازش مسیرها مهم است؟
در پیادهسازی ضعیف، پردازش کلیدها براساس ترتیب ورود میتواند نتیجه را تغییر دهد. اگر ابتدا user با یک String ثبت شود و بعد user.name برسد، برنامه نمیتواند همزمان user را مقدار نهایی و Object در نظر بگیرد. ابزار قابل اعتماد باید این تعارض را صریح گزارش کند و نباید بیصدا مقدار قبلی را حذف کند.
قرارداد مسیر؛ مهمترین بخش Unflatten
کلید تخت بدون قرارداد فقط یک String است. نقطه، اسلش، براکت یا ترکیبی از آنها ممکن است جداکننده باشد. دو سیستم حتی با داده یکسان میتوانند خروجیهای متفاوت تولید کنند.
| قالب مسیر | نمونه | مزیت | ریسک |
|---|---|---|---|
| Dot notation | user.name | ساده و خوانا | ابهام با کلید واقعی دارای نقطه |
| Bracket notation | items[0].sku | نمایش روشن Array | Parser پیچیدهتر |
| Slash path | user/address/city | مناسب مسیرهای سلسلهمراتبی | نیاز به Escape اسلش |
| JSON Pointer-like | /user/address/city | قرارداد دقیقتر مسیر | نیاز به قواعد Escape مشخص |
کلیدهایی که خودشان جداکننده دارند
فرض کنید داده اصلی کلیدی به نام app.version دارد. اگر Flatten آن را بدون Escape ذخیره کند، Unflatten ممکن است Objectی به نام app با فیلد version بسازد. برای جلوگیری از این خطا باید یکی از روشهای زیر انتخاب شود: Escape جداکننده، استفاده از قالب مسیر دارای Quote، نگهداری Metadata مسیر یا ممنوعکردن کلیدهای مبهم در قرارداد داده.
{
"settings.app\.version": "2.1"
}
در این مثال، بکاسلش نشان میدهد نقطه بخشی از نام کلید است، نه جداکننده مسیر. البته این رفتار استاندارد عمومی JSON نیست و فقط زمانی معتبر است که هر دو طرف دقیقاً همین قرارداد را اجرا کنند.
بازسازی Arrayها در JSON Unflatten
Arrayها دشوارترین بخش Unflatten هستند، زیرا عدد داخل مسیر ممکن است ایندکس Array یا نام یک Property باشد. مسیر items.0.name در یک سیستم به Array تبدیل میشود، اما سیستم دیگر ممکن است Objectی با کلید 0 بسازد.
{
"items[0].sku": "A-10",
"items[0].quantity": 2,
"items[1].sku": "B-20",
"items[1].quantity": 1
}
خروجی مطلوب:
{
"items": [
{ "sku": "A-10", "quantity": 2 },
{ "sku": "B-20", "quantity": 1 }
]
}
ایندکسهای خالی و Sparse Array
اگر فقط items[3] وجود داشته باشد، ابزار باید تصمیم بگیرد سه خانه قبلی را چگونه نمایش دهد. بعضی پیادهسازیها Array خالی با Slotهای خالی میسازند، برخی null قرار میدهند و برخی کل ساختار را Object نگه میدارند. این تفاوت در Serialization بعدی قابل مشاهده است و باید در قرارداد مستند شود.
عدد بهعنوان نام کلید
Object زیر کاملاً ممکن است:
{
"years": {
"2024": 120,
"2025": 180
}
}
اگر هر Token عددی خودکار Array تلقی شود، این داده اشتباه بازسازی خواهد شد. قالب براکتی برای Array یا Metadata نوع Container میتواند این ابهام را کاهش دهد.
مثال واقعی: بازسازی رکورد سفارش
فرض کنید خروجی یک سیستم گزارشگیری برای هر سفارش به شکل تخت ذخیره شده است:
{
"order.id": "ORD-1042",
"order.customer.name": "Nima",
"order.customer.vip": false,
"order.items[0].sku": "KB-01",
"order.items[0].price": 950000,
"order.items[1].sku": "MS-08",
"order.items[1].price": 420000,
"order.discount": null
}
Unflatten صحیح باید String، Boolean، Number و null را حفظ کند:
{
"order": {
"id": "ORD-1042",
"customer": {
"name": "Nima",
"vip": false
},
"items": [
{ "sku": "KB-01", "price": 950000 },
{ "sku": "MS-08", "price": 420000 }
],
"discount": null
}
}
اگر مقادیر از CSV آمده باشند، ممکن است همه آنها String باشند. Unflatten بهتنهایی نباید حدس بزند که "false" Boolean یا "950000" Number است، مگر اینکه تبدیل نوع بخشی صریح از تنظیمات باشد. بازسازی ساختار و تبدیل نوع دو مسئولیت جدا هستند.
پیادهسازی ساده JSON Unflatten در JavaScript
نمونه زیر مسیرهای نقطهای ساده را به Object تبدیل میکند. این نسخه برای آموزش است و Array، Escape و تعارض مسیر را کامل پوشش نمیدهد:
function unflatten(flatObject) {
const result = {};
for (const [path, value] of Object.entries(flatObject)) {
const parts = path.split(".");
let current = result;
parts.forEach((part, index) => {
const isLast = index === parts.length - 1;
if (isLast) {
current[part] = value;
return;
}
if (
current[part] === undefined ||
current[part] === null ||
typeof current[part] !== "object" ||
Array.isArray(current[part])
) {
current[part] = {};
}
current = current[part];
});
}
return result;
}
برای استفاده واقعی باید قبل از جایگزینی یک مقدار موجود، تعارض گزارش شود. همچنین استفاده مستقیم از کلیدهای ورودی بدون محافظت میتواند مشکل امنیتی ایجاد کند.
پیادهسازی ساده در Python
def unflatten(flat_data, separator="."):
result = {}
for path, value in flat_data.items():
parts = path.split(separator)
current = result
for part in parts[:-1]:
existing = current.get(part)
if existing is None:
current[part] = {}
elif not isinstance(existing, dict):
raise ValueError(f"Path conflict at: {part}")
current = current[part]
leaf = parts[-1]
if leaf in current and isinstance(current[leaf], dict):
raise ValueError(f"Path conflict at: {path}")
current[leaf] = value
return result
این پیادهسازی تعارض ساده را بهتر مدیریت میکند، اما همچنان Array و Escape جداکننده را ندارد. در پروژه واقعی، Parser مسیر بهتر است بهجای split ساده، Tokenizer مشخص داشته باشد.
برخورد مسیرها و دادههای متناقض
یکی از خطاهای مهم زمانی رخ میدهد که یک مسیر هم مقدار نهایی و هم والد مسیر دیگری باشد:
{
"user": "guest",
"user.name": "Ali"
}
هیچ ساختار JSON واحدی نمیتواند user را هم String و هم Object نگه دارد. سیاستهای ممکن عبارتاند از:
- توقف و گزارش خطا؛ امنترین گزینه برای داده حساس.
- اولویتدادن به مسیر عمیق و حذف مقدار والد؛ خطر از دست رفتن داده.
- اولویتدادن به مقدار والد و نادیدهگرفتن فرزند؛ باز هم از دست رفتن داده.
- انتقال مقدار والد به کلیدی رزروشده؛ فقط با قرارداد از پیش تعیینشده.
رفتار پیشفرض مناسب، Fail Fast است. ابزار نباید بدون هشدار یکی از مقادیر را حذف کند.
null، Object خالی و Array خالی
در داده تخت، تفاوت میان Container خالی و نبودن مسیر بهسادگی گم میشود. برای مثال Object خالی {} هیچ برگ قابل ذخیرهای ندارد. اگر Flatten فقط مقدارهای نهایی را ثبت کند، Object خالی از خروجی حذف میشود و Unflatten نمیتواند آن را بازگرداند.
| ساختار اصلی | چالش در داده تخت | راهحل ممکن |
|---|---|---|
null | ممکن است با نبود مقدار اشتباه شود | ثبت صریح null |
{} | هیچ مسیر برگی ندارد | Marker نوع Container |
[] | هیچ ایندکسی ندارد | Marker Array خالی |
| Missing | ممکن است با null یکی شود | تمایز در قرارداد ورودی |
تست Round Trip برای اطمینان از بازسازی
معتبر بودن JSON خروجی کافی نیست. ممکن است خروجی از نظر Syntax صحیح باشد اما با داده اصلی تفاوت داشته باشد. تست Round Trip این مراحل را دارد:
- یک مجموعه نمونه شامل Object، Array، null، کلیدهای خاص و Container خالی آماده کنید.
- داده را با قرارداد مشخص Flatten کنید.
- خروجی را با همان تنظیمات Unflatten کنید.
- نتیجه را با داده اصلی بهصورت ساختاری مقایسه کنید.
- اختلاف نوع، ترتیب Array، کلیدهای گمشده و Containerهای خالی را بررسی کنید.
مقایسه متنی همیشه مناسب نیست، زیرا ترتیب کلیدهای Object معمولاً معنای داده را تغییر نمیدهد. در مقابل، ترتیب عناصر Array بخشی از معناست و باید دقیق حفظ شود. برای بررسی خروجی میتوان از ابزار مقایسه دو JSON استفاده کرد.
خطاهای رایج در JSON Unflatten
استفاده از split ساده برای همه مسیرها
این روش کلیدهای دارای جداکننده را میشکند. Tokenizer باید Escape، Quote یا قالب براکتی را بشناسد.
تبدیل خودکار هر عدد به ایندکس Array
کلید عددی در Object قانونی است. Array باید با نشانه صریح یا Metadata تشخیص داده شود.
نادیدهگرفتن برخورد مسیرها
جایگزینی بیصدای String با Object باعث حذف داده میشود. تعارض باید خطا یا حداقل Warning قابل مشاهده ایجاد کند.
تبدیل همه مقادیر به String
Unflatten باید نوع موجود را حفظ کند. تبدیل نوع باید مرحلهای جدا و کنترلشده باشد.
فراموشکردن Object و Array خالی
بدون Marker یا Metadata، بازسازی کامل Containerهای خالی ممکن نیست.
اعتماد به ترتیب کلیدهای ورودی
نتیجه نباید با جابهجایی ترتیب Propertyها تغییر کند. الگوریتم باید تعارض را مستقل از ترتیب تشخیص دهد.
نکات امنیتی JSON Unflatten
ساخت Propertyهای تودرتو از ورودی کنترلنشده میتواند خطرناک باشد. در JavaScript، مسیرهایی مانند __proto__، constructor.prototype یا کلیدهای خاص مشابه ممکن است به Prototype Pollution منجر شوند. پیادهسازی امن باید Tokenهای خطرناک را رد کند، از Objectهای بدون Prototype استفاده کند یا کتابخانهای بهروز با محافظت روشن انتخاب کند.
- طول کل مسیر و تعداد Tokenها را محدود کنید.
- حداکثر عمق ساختار خروجی را تعیین کنید.
- تعداد کلیدها و بیشترین ایندکس Array را محدود کنید.
- کلیدهای رزروشده و خطرناک را مسدود کنید.
- قبل از پردازش داده حساس، ورودی را در محیط کنترلشده بررسی کنید.
- خطاها را بدون نمایش محتوای محرمانه Log کنید.
حمله با ایندکس بسیار بزرگ
ورودی مانند items[999999999] میتواند برنامهای را که Array واقعی تخصیص میدهد با مصرف شدید حافظه مواجه کند. ابزار باید حداکثر ایندکس مجاز داشته باشد یا برای ایندکسهای پراکنده از سیاست امنتری استفاده کند.
عملکرد و JSONهای بزرگ
در حالت معمول، زمان اجرا نسبت مستقیم با مجموع تعداد Tokenهای مسیر دارد. با این حال ساخت مکرر String، Tokenize کردن مسیرهای طولانی و ایجاد Containerهای زیاد میتواند هزینه حافظه را افزایش دهد. برای داده بزرگ:
- مسیر را فقط یکبار Parse کنید.
- از محدودیت عمق و تعداد کلید استفاده کنید.
- تعارض را هنگام درج تشخیص دهید، نه در یک پیمایش اضافی.
- برای Datasetهای بسیار بزرگ پردازش دستهای را در نظر بگیرید.
- قبل از ساخت Array، بیشترین ایندکس و پراکندگی را بررسی کنید.
Unflatten معمولاً نیازمند نگهداری ساختار خروجی در حافظه است. اگر خروجی نهایی بسیار بزرگ باشد، پردازش Streaming کامل دشوار میشود، زیرا مسیرهای مختلف میتوانند به شاخههای مشترک یا ایندکسهای دور از هم اشاره کنند.
بهترین روشهای JSON Unflatten
- قرارداد مسیر را همراه داده نسخهبندی کنید.
- Object و Array را با نشانههای بدون ابهام تفکیک کنید.
- Escape کلیدهای دارای جداکننده را اجباری کنید.
- در تعارض مسیر، عملیات را متوقف و خطا را گزارش کنید.
- نوع مقدارها را بدون درخواست صریح تغییر ندهید.
- برای null، Container خالی و Missing سیاست جدا داشته باشید.
- تست Round Trip و تست امنیتی کلیدهای خاص بنویسید.
- خروجی را پس از بازسازی با قواعد Syntax JSON و در صورت وجود با Schema بررسی کنید.
چکلیست انتخاب یا پیادهسازی ابزار Unflatten
- آیا جداکننده مسیر قابل تنظیم است؟
- آیا Dot و Bracket notation بهدرستی Parse میشوند؟
- آیا کلیدهای دارای نقطه یا براکت Escape میشوند؟
- آیا Array با Object دارای کلید عددی اشتباه نمیشود؟
- آیا برخورد مسیرها خطای واضح ایجاد میکند؟
- آیا String، Number، Boolean و null حفظ میشوند؟
- آیا برای Object و Array خالی راهحل وجود دارد؟
- آیا محدودیت عمق، تعداد کلید و ایندکس Array اعمال میشود؟
- آیا کلیدهای خطرناک JavaScript مسدود هستند؟
- آیا Round Trip روی نمونههای واقعی موفق است؟
جمعبندی
JSON Unflatten راهی برای تبدیل کلیدهای مسیرمحور به Objectها و Arrayهای تودرتو است، اما نتیجه صحیح فقط زمانی به دست میآید که قرارداد مسیر دقیق باشد. جداکننده، Escape کلیدها، نمایش Array، نوع مقدارها، Containerهای خالی و تعارض مسیرها همگی بر خروجی اثر دارند. در سیستمهای واقعی، موفقیت Parse یا معتبر بودن Syntax تضمین نمیکند که ساختار اصلی درست بازسازی شده باشد؛ تست Round Trip و مقایسه ساختاری معیار قابل اعتمادتری هستند. برای بازسازی سریع داده تخت میتوانید از ابزار JSON Unflatten استفاده کنید و سپس خروجی را با Viewer، Validator یا Schema بررسی کنید.