التخطي إلى المحتوى
Toova
جميع الأدوات

كيفية تنسيق JSON في JavaScript — 5 طرق

Toova

JSON موجود في كل مكان: استجابات API وملفات الإعداد وصادرات قواعد البيانات وقنوات السجلات. لكن مخرجات JSON الخام كثيراً ما تكون كتلة مضغوطة — بلا فواصل أسطر ولا مسافات بادئة والمفاتيح بأي ترتيب يُخرجه المُسلسِل. هذا يُصعِّب التصحيح ويُعقِّد التعاون أكثر مما ينبغي.

يمنحك JavaScript عدة طرق لتنسيق JSON، من JSON.stringify المُدمَج إلى مكتبات التدفق للملفات الكبيرة التي يتعذَّر تحميلها في الذاكرة. يتناول هذا الدليل خمس طرق عملية تغطي الحالات الأكثر شيوعاً والحالات الحافة التي تُفاجئ المطورين.

الطريقة 1: JSON.stringify مع المسافة البادئة

أبسط أسلوب مُدمَج في اللغة أصلاً. تقبل JSON.stringify ثلاثة وسائط: القيمة للتسلسل، ودالة replacer (المزيد عنها في الطريقة 2)، ومعامل space الذي يتحكم في المسافة البادئة.

const data = {
  name: "Alice",
  role: "engineer",
  skills: ["JavaScript", "TypeScript", "Node.js"],
  active: true,
};

// 2-space indent (common default)
console.log(JSON.stringify(data, null, 2));

// 4-space indent
console.log(JSON.stringify(data, null, 4));

// Tab indent
console.log(JSON.stringify(data, null, "\t"));

مع null, 2، يبدو المخرج كالتالي:

{
  "name": "Alice",
  "role": "engineer",
  "skills": [
    "JavaScript",
    "TypeScript",
    "Node.js"
  ],
  "active": true
}

مسافتان هو الاتفاق الأكثر شيوعاً في مشاريع JavaScript — يطابق الافتراضي في ESLint وPrettier ومعظم دلائل الأسلوب. أربع مسافات شائعة في الأدوات المجاورة لـ Python. عند إغفال space أو ضبطه على 0، يكون المخرج سلسلة مضغوطة في سطر واحد — مثالية للنقل عبر الشبكة.

يمكن أيضاً تنسيق JSON فوراً في المتصفح باستخدام أداة منسِّق JSON — الصق JSON الخام، احصل على مخرج منسَّق، وانسخ النتيجة دون كتابة أي كود.

الطريقة 2: دالة Replacer المخصصة

الوسيط الثاني لـ JSON.stringify — الـ replacer — هو دالة أو مصفوفة تُصفِّي القيم وتُحوِّلها قبل التسلسل. هنا تحصل على تحكم حقيقي في ما يظهر في المخرج.

الشكل المصفوفي هو الأبسط لإدراج المفاتيح في قائمة بيضاء:

const user = {
  id: 42,
  name: "Bob",
  password: "s3cr3t",
  email: "bob@example.com",
  createdAt: new Date(),
};

// Array replacer: include only listed keys
const safeJson = JSON.stringify(user, ["id", "name", "email"], 2);
console.log(safeJson);
// {
//   "id": 42,
//   "name": "Bob",
//   "email": "bob@example.com"
// }

الشكل الدالي يمنحك تحكماً لكل قيمة. أعِد القيمة لتضمينها، أعِد undefined لحذفها، أو أعِد قيمة محوَّلة:

function replacer(key, value) {
  // Drop keys that start with underscore (private convention)
  if (key.startsWith("_")) return undefined;

  // Mask sensitive fields
  if (key === "password" || key === "token") return "[REDACTED]";

  // Convert Date objects to ISO strings explicitly
  if (value instanceof Date) return value.toISOString();

  return value;
}

const payload = {
  id: 1,
  name: "Carol",
  password: "hunter2",
  _internalFlag: true,
  lastLogin: new Date("2026-05-01"),
};

console.log(JSON.stringify(payload, replacer, 2));
// {
//   "id": 1,
//   "name": "Carol",
//   "password": "[REDACTED]",
//   "lastLogin": "2026-05-01T00:00:00.000Z"
// }

يعمل الـ replacer بشكل تكراري على كل كائن ومصفوفة متداخلة. هذا النمط مفيد لقنوات السجلات حيث تريد إزالة بيانات الاعتماد قبل الكتابة على القرص أو الإرسال إلى خدمة المراقبة.

الطريقة 3: الطباعة المنسَّقة مع مفاتيح مُرتَّبة

لا تضمن كائنات JavaScript ترتيب المفاتيح (رغم أن V8 ومعظم المحركات تحفظ ترتيب الإدراج للمفاتيح النصية). عندما تحتاج إلى مخرج حتمي — للمقارنة أو التخزين المؤقت أو التمثيلات العرفية — يكون ترتيب المفاتيح أبجدياً هو الخيار الصحيح.

function sortedStringify(value, indent = 2) {
  return JSON.stringify(value, sortReplacer, indent);
}

function sortReplacer(key, value) {
  if (value !== null && typeof value === "object" && !Array.isArray(value)) {
    return Object.keys(value)
      .sort()
      .reduce((sorted, k) => {
        sorted[k] = value[k];
        return sorted;
      }, {});
  }
  return value;
}

const config = {
  version: "1.0",
  author: "Dave",
  dependencies: { typescript: "^5.4", eslint: "^9.0", astro: "^5.0" },
  name: "my-project",
};

console.log(sortedStringify(config));
// {
//   "author": "Dave",
//   "dependencies": { "astro": "^5.0", "eslint": "^9.0", "typescript": "^5.4" },
//   "name": "my-project",
//   "version": "1.0"
// }

المفاتيح المُرتَّبة تعني أن فروق git تُظهر فقط الأسطر التي تغيَّرت فعلاً، لا إعادة ترتيبات عشوائية من مُسلسِلات مختلفة. هذا مفيد بشكل خاص لـ package.json والملفات المماثلة المُضمَّنة في نظام التحكم بالإصدارات.

لتحويل JSON إلى صيغ أخرى، تتعامل أداة JSON إلى YAML وأداة JSON إلى CSV أيضاً مع ترتيب المفاتيح في مخرجاتهما.

الطريقة 4: التنسيق من سلسلة نصية (Parse + Stringify)

JSON في العالم الحقيقي يصل عادةً كسلسلة نصية — من استجابة fetch أو قراءة ملف أو لصق من الحافظة أو عمود TEXT في قاعدة بيانات. تحتاج إلى عمل parse أولاً ثم إعادة تنسيقه. العنصر الحاسم هو معالجة الأخطاء الصحيحة: JSON غير الصالح سيرمي استثناءً وتريد التقاطه بسلاسة.

function formatJsonString(rawString, indent = 2) {
  try {
    const parsed = JSON.parse(rawString);
    return { ok: true, result: JSON.stringify(parsed, null, indent) };
  } catch (err) {
    return { ok: false, error: err.message };
  }
}

const raw = '{"name":"Eve","scores":[100,95,88],"active":true}';
const { ok, result, error } = formatJsonString(raw);

if (ok) {
  console.log(result);
  // {
  //   "name": "Eve",
  //   "scores": [100, 95, 88],
  //   "active": true
  // }
} else {
  console.error("Parsing failed:", error);
}

// Invalid input
const bad = '{"name": "Eve", "broken":}';
const r2 = formatJsonString(bad);
// { ok: false, error: "Unexpected token '}'" }

تغليف أخطاء التحليل في كائن إرجاع منظَّم يجعل هذه الدالة آمنة للاستخدام في مكونات واجهة المستخدم وسكريبتات البناء دون محاطة كل موقع استدعاء بـ try/catch. تغطي وثائق MDN لـ JSON.stringify المواصفة الكاملة للمعاملات، ويُعرِّف RFC 8259 ما يبدو عليه JSON الصالح على المستوى البروتوكولي.

الطريقة 5: التدفق للملفات الكبيرة

الطرق 1-4 جميعها تُحمِّل بنية JSON الكاملة في الذاكرة قبل التنسيق. للملفات بمئات الميغابايتات أو متعددة الغيغابايتات، يحجب هذا حلقة أحداث Node.js وقد يُعطِّل العملية كلياً.

الأسلوب التدفقي يقرأ الملف على شكل أجزاء ويكتب المخرج المنسَّق تدريجياً. لـ NDJSON (كائن JSON واحد لكل سطر، شائع في ملفات السجلات وصادرات قواعد البيانات)، يعمل أسلوب readline دون تبعيات إضافية:

import { createReadStream, createWriteStream } from "node:fs";
import { createInterface } from "node:readline";

async function formatNdjsonFile(inputPath, outputPath) {
  const rl = createInterface({
    input: createReadStream(inputPath),
    crlfDelay: Infinity,
  });

  const output = createWriteStream(outputPath);

  for await (const line of rl) {
    if (!line.trim()) continue;
    try {
      const obj = JSON.parse(line);
      output.write(JSON.stringify(obj, null, 2) + "\n---\n");
    } catch (e) {
      output.write("[Invalid JSON line: " + e.message + "]\n---\n");
    }
  }

  output.end();
}

NDJSON هو أبسط صيغ التدفق: كل سطر كائن JSON صالح ومكتمل. تدعمه كثير من أدوات التصدير تحديداً لأنه قابل للتدفق بسهولة. إذا كنت تتحكم في صيغة صادرات البيانات الكبيرة، فضِّل NDJSON على مصفوفة JSON ضخمة واحدة.

حالات حافة يجب مراعاتها

هذه السيناريوهات التي تفشل فيها معالجة JSON القياسية بصمت أو ترمي استثناءً بشكل غير متوقع.

المراجع الدائرية

ترمي JSON.stringify خطأ TypeError إذا أشار كائن إلى نفسه مباشرةً أو غير مباشر. أصلحها بدالة replacer تتتبع الكائنات التي تمت زيارتها باستخدام WeakSet:

function safeStringify(obj, indent = 2) {
  const seen = new WeakSet();
  return JSON.stringify(obj, (key, value) => {
    if (typeof value === "object" && value !== null) {
      if (seen.has(value)) return "[Circular]";
      seen.add(value);
    }
    return value;
  }, indent);
}

const a = { name: "circular" };
a.self = a;
console.log(safeStringify(a));
// { "name": "circular", "self": "[Circular]" }

تحتفظ WeakSet بالمراجع دون منع جمع القمامة، مما يتجنب تسرُّبات الذاكرة في العمليات طويلة الأمد.

قيم BigInt

ترمي JSON.stringify خطأ TypeError لقيم BigInt لأن مواصفة JSON لا تحتوي نوع عدد صحيح 64 بت. حوِّلها إلى سلسلة في الـ replacer:

const data = { id: 9007199254740993n, value: 42 };

JSON.stringify(data, (key, value) =>
  typeof value === "bigint" ? value.toString() : value
, 2);
// { "id": "9007199254740993", "value": 42 }

قيم Map وSet

تُسلسَل Map ككائن فارغ وSet كمصفوفة فارغة — لا كمحتوياتهما. حوِّلهما صراحةً في الـ replacer:

const data = {
  tags: new Set(["json", "javascript"]),
  meta: new Map([["source", "api"]]),
};

JSON.stringify(data, (key, value) => {
  if (value instanceof Set) return [...value];
  if (value instanceof Map) return Object.fromEntries(value);
  return value;
}, 2);
// { "tags": ["json", "javascript"], "meta": { "source": "api" } }

قيم undefined

خصائص الكائن ذات قيم undefined تُحذف بصمت. عناصر المصفوفة ذات قيمة undefined تصبح null. استخدم replacer لتحويل undefined إلى null عندما تحتاج إلى الاحتفاظ بجميع المفاتيح:

const obj = { a: 1, b: undefined, c: null };
JSON.stringify(obj, null, 2);
// { "a": 1, "c": null }  — "b" is silently dropped

// Fix:
JSON.stringify(obj, (key, value) =>
  value === undefined ? null : value
, 2);
// { "a": 1, "b": null, "c": null }

تنسيق JSON فوراً في المتصفح

إذا كنت بحاجة إلى تنسيق مجموعة JSON الآن دون كتابة أي كود، تُعالجها أداة Toova لتنسيق JSON بنقرة واحدة — الصق JSON الخام، احصل على مخرج منسَّق بمسافة 2 أو 4 مسافات، وانسخ النتيجة. بلا تسجيل، بلا رفع ملفات، كل شيء يعمل محلياً في متصفحك.

للتحويل بين الصيغ، تتبع JSON إلى YAML وJSON إلى CSV النهج ذاته القائم على الخصوصية أولاً — بياناتك لا تغادر جهازك.

الخلاصة

لمعظم حالات الاستخدام، JSON.stringify(obj, null, 2) هو كل ما تحتاجه. أضِف دالة replacer عندما تحتاج إلى تصفية أو إخفاء أو مفاتيح مُرتَّبة. لُفَّ JSON.parse في try/catch عند التعامل مع مدخلات خارجية. انتقل إلى التدفق فقط عندما يجعل حجم الملف التحليل المتزامن غير عملي. واحتفظ في ذهنك بالحالات الحافة — المراجع الدائرية وBigInt وMap/Set وundefined — عند العمل مع أشكال بيانات غير معتادة.