وقتی دادهها بهصورت جریان پیوسته، لاگ، رویداد یا مجموعهای بسیار بزرگ از رکوردها تولید میشوند، قرار دادن همه آنها داخل یک Array بزرگ JSON همیشه انتخاب مناسبی نیست. در چنین سناریوهایی JSON Lines یا NDJSON قالبی ساده و کاربردی است که در آن هر خط یک مقدار JSON مستقل و معتبر را نگه میدارد. این ساختار امکان پردازش رکوردبهرکورد، Streaming، ادامهدادن پردازش پس از خطا و کار با فایلهای بسیار بزرگ را آسانتر میکند.
پاسخ سریع: JSON Lines و NDJSON دو نام رایج برای قالبی هستند که در آن هر خط یک سند JSON مستقل است. برخلاف JSON معمولی، کل فایل یک Array واحد نیست؛ بنابراین میتوان فایل را خطبهخط خواند، بدون اینکه تمام محتوا وارد حافظه شود.
JSON Lines و NDJSON چیست؟
در JSON استاندارد، اگر بخواهیم چند رکورد را کنار هم ذخیره کنیم معمولاً آنها را داخل یک Array قرار میدهیم. در JSON Lines هر رکورد در یک خط جدا نوشته میشود و میان رکوردها فقط کاراکتر پایان خط قرار دارد.
{"id":1,"name":"Ali","active":true}
{"id":2,"name":"Sara","active":false}
{"id":3,"name":"Reza","active":true}
هر خط بالا بهتنهایی JSON معتبر است، اما کل فایل از دید Parser معمول JSON یک سند JSON واحد محسوب نمیشود. برای خواندن آن باید فایل را خطبهخط تقسیم و هر خط را جداگانه Parse کرد.
تفاوت JSON Lines، NDJSON و JSON معمولی
| ویژگی | JSON Array | JSON Lines / NDJSON |
|---|---|---|
| ساختار کل فایل | یک سند JSON واحد | چند سند مستقل، هر خط یک سند |
| شروع و پایان | نیازمند براکت باز و بسته | بدون Wrapper کلی |
| پردازش Streaming | دشوارتر | طبیعی و ساده |
| مصرف حافظه | اغلب نیازمند Parse کل سند | قابل پردازش رکوردبهرکورد |
| تحمل خطا | یک Syntax Error ممکن است کل سند را خراب کند | میتوان فقط خط خراب را رد یا ثبت کرد |
| ویرایش Append | نیازمند تغییر انتهای Array | افزودن یک خط جدید کافی است |
اصطلاحهای JSON Lines، newline-delimited JSON و NDJSON در عمل برای یک ایده مشابه استفاده میشوند. تفاوت اصلی معمولاً در نامگذاری، پسوند فایل و قرارداد ابزارهاست، نه در مدل داده.
قوانین ساخت فایل NDJSON معتبر
هر خط باید یک مقدار JSON کامل باشد
هر خط باید مستقل Parse شود. Object رایجترین انتخاب است، اما از نظر فنی Array، String، Number، Boolean و null نیز میتوانند یک خط مستقل باشند. با این حال برای سازگاری بهتر، بیشتر Pipelineها انتظار Object دارند.
رکورد نباید در چند خط شکسته شود
Pretty Print کردن یک Object و شکستن آن در چند خط، مرز رکوردها را مبهم میکند. هر رکورد باید در یک خط فشرده باقی بماند.
{"id":1,"profile":{"city":"Tehran","age":29}}
خط خالی را با قرارداد مشخص مدیریت کنید
بعضی Readerها خط خالی را نادیده میگیرند و بعضی آن را خطا میدانند. برای Pipeline قابل پیشبینی بهتر است خط خالی تولید نشود یا رفتار Reader صریحاً مشخص باشد.
Encoding را ثابت نگه دارید
UTF-8 انتخاب معمول و مناسب است. ترکیب Encodingهای متفاوت یا وجود کاراکترهای خراب میتواند باعث شکست Parse یک خط شود، حتی وقتی ساختار ظاهری آن درست است.
مثال صحیح و غلط
نمونه صحیح
{"event":"login","userId":42,"success":true}
{"event":"purchase","userId":42,"amount":125000}
{"event":"logout","userId":42}
نمونه غلط: قرار دادن کاما میان خطوط
{"id":1},
{"id":2},
{"id":3}
کاما بخشی از JSON Array است و در NDJSON نباید پس از هر رکورد قرار بگیرد.
نمونه غلط: شکستن یک رکورد در چند خط
{
"id": 1,
"name": "Ali"
}
این متن از نظر JSON معمولی معتبر است، اما برای Reader خطمحور چهار خط جدا ایجاد میکند و معمولاً نامعتبر خواهد بود.
کاربردهای واقعی JSON Lines
- لاگهای ساختاریافته: هر رویداد در یک خط نوشته میشود و ابزار جمعآوری لاگ آن را مستقل پردازش میکند.
- خروجی پردازش داده: Jobهای طولانی میتوانند نتایج را بهتدریج Append کنند.
- Export و Import حجیم: میلیونها رکورد بدون ساخت Array بزرگ انتقال مییابد.
- Machine Learning: نمونههای آموزشی و پاسخها بهصورت رکوردهای مستقل ذخیره میشوند.
- Event Stream: هر پیام یا Event یک Object مستقل است.
- Command-line Pipelines: ابزارها میتوانند ورودی را خطبهخط Filter، Map یا Aggregate کنند.
خواندن NDJSON در JavaScript
برای فایل کوچک میتوان متن را بر اساس Line Break تقسیم کرد، اما برای فایل بزرگ بهتر است از Stream استفاده شود.
const lines = text.split(/\r?\n/);
const records = [];
for (const line of lines) {
if (!line.trim()) continue;
records.push(JSON.parse(line));
}
در Node.js میتوان از Readline استفاده کرد تا فایل بدون بارگذاری کامل پردازش شود:
import fs from 'node:fs';
import readline from 'node:readline';
const input = fs.createReadStream('events.ndjson');
const rl = readline.createInterface({ input, crlfDelay: Infinity });
for await (const line of rl) {
if (!line.trim()) continue;
try {
const record = JSON.parse(line);
console.log(record.id);
} catch (error) {
console.error('Invalid line:', line);
}
}
خواندن NDJSON در Python
import json
with open('events.ndjson', encoding='utf-8') as file:
for line_number, line in enumerate(file, start=1):
if not line.strip():
continue
try:
record = json.loads(line)
print(record.get('id'))
except json.JSONDecodeError as error:
print(f'Invalid JSON at line {line_number}: {error}')
این الگو فقط یک خط را در هر لحظه نگه میدارد و برای فایلهای بزرگ مناسبتر از json.load روی یک Array حجیم است.
تبدیل JSON Array به NDJSON
برای تبدیل، هر عضو Array را جداگانه Stringify و سپس با Newline به خروجی اضافه کنید.
const output = records
.map(record => JSON.stringify(record))
.join('\n');
در دادههای بسیار بزرگ بهتر است رکوردها بهتدریج نوشته شوند تا Array خروجی و String نهایی همزمان در حافظه ساخته نشوند.
تبدیل NDJSON به JSON Array
برای مجموعه کوچک میتوان تمام خطوط را Parse و داخل Array قرار داد. در مجموعه حجیم، این تبدیل مزیت Streaming را از بین میبرد و باید فقط زمانی انجام شود که مصرفکننده واقعاً به یک سند JSON واحد نیاز دارد.
const array = text
.split(/\r?\n/)
.filter(line => line.trim())
.map(line => JSON.parse(line));
مدیریت خطا و Troubleshooting
خطای Unexpected token
معمولاً یک خط دارای کامای اضافی، Quote بستهنشده یا Escape نامعتبر است. شماره خط را ثبت و همان خط را جداگانه در Validator بررسی کنید.
رکوردهای ناقص
اگر نویسنده فایل هنگام نوشتن متوقف شود، آخرین خط ممکن است ناقص بماند. برای فایلهای حساس از نوشتن اتمیک، فایل موقت یا Checkpoint استفاده کنید.
Line Ending متفاوت
Reader باید هر دو حالت LF و CRLF را مدیریت کند. تقسیم صرفاً بر اساس
معمولاً کار میکند، اما ممکن است کاراکتر
در انتهای خط باقی بماند.
وجود BOM
در برخی فایلهای UTF-8 ممکن است BOM در ابتدای خط اول ظاهر شود و Parse را خراب کند. Reader باید آن را حذف یا Encoding ورودی را درست تشخیص دهد.
امنیت در پردازش NDJSON
- برای طول هر خط سقف تعیین کنید تا یک رکورد بسیار بزرگ باعث مصرف شدید حافظه نشود.
- تعداد رکورد، عمق Object و اندازه Stringها را محدود کنید.
- داده Parseشده را مستقیماً به Query، Template یا Command تزریق نکنید.
- خط خراب را همراه با داده حساس در Log عمومی ثبت نکنید.
- در Pipelineهای غیرقابل اعتماد، Schema Validation را بعد از Syntax Validation اجرا کنید.
نکات عملکرد و Best Practice
- فایل را Stream کنید و از خواندن کامل آن در حافظه بپرهیزید.
- هر خط را مستقل Validate کنید و شماره خط را در خطا نگه دارید.
- برای Backpressure برنامه داشته باشید تا Producer از Consumer جلو نزند.
- از Objectهای همشکل و فیلدهای پایدار استفاده کنید تا پردازش سادهتر شود.
- در صورت نیاز به فشردهسازی، Compression سطح انتقال یا فایل را فعال کنید.
- قرارداد خط خالی، Line Ending، Encoding و حداکثر اندازه رکورد را مستند کنید.
چه زمانی NDJSON انتخاب مناسبی نیست؟
اگر کل داده یک سند واحد با روابط ساختاری عمیق است، یا مصرفکننده فقط JSON استاندارد میپذیرد، Array معمولی مناسبتر است. همچنین اگر رکوردها برای تفسیر به Context مشترک وابستهاند، جداسازی خطی ممکن است باعث پیچیدگی قرارداد شود.
چکلیست استفاده از JSON Lines
- هر خط یک JSON کامل و مستقل است.
- میان رکوردها کاما وجود ندارد.
- رکوردها Pretty Print نشدهاند.
- Encoding و Line Ending مشخص است.
- خطا با شماره خط گزارش میشود.
- حداکثر اندازه هر رکورد تعریف شده است.
- Schema یا قرارداد فیلدها مشخص است.
- پردازش فایل حجیم بهصورت Streaming انجام میشود.
جمعبندی
JSON Lines و NDJSON راهی ساده برای ذخیره و انتقال مجموعهای از رکوردهای JSON مستقل هستند. مزیت اصلی آنها پردازش خطبهخط، Append ساده، مصرف حافظه کمتر و جداسازی خطاهاست. در مقابل، این قالب یک سند JSON استاندارد واحد نیست و باید با Reader سازگار با NDJSON خوانده شود. برای لاگ، Event Stream، Export حجیم و Pipelineهای داده، NDJSON اغلب انتخابی عملیتر از یک Array بسیار بزرگ است.