خطای json_decode در PHP یکی از آن خطاهایی است که در نگاه اول شاید ساده به نظر برسد ولی در عمل، می‌تواند منبع ساعاتی عیب‌یابی و هزینه‌ساز باشد. اولین باری که این خطا را در یک پروژه واقعی دیدم، در افزونه‌ای بود که داده‌های نرخ ارز را از یک API بیرونی دریافت می‌کرد و روی هاستی اجرا می‌شد که سرور مقصد، پاسخ را با یک بایت اضافی در ابتدا ارسال می‌کرد. نتیجه این بایت اضافی، یک Object خالی بود که تا هفته‌ها باعث نمایش قیمت‌های صفر در سایت شده بود و کسی متوجه نشده بود چون خطای سطحی JSON رخ نمی‌داد.

اگر با مفاهیم پایه PHP در وردپرس آشنایی کمتری دارید، پیش از ادامه PHP چیست و چگونه از آن استفاده کنیم؟ را بخوانید. این نوشته لایه عیب‌یابی همان بحث است. برای درک ارتباط این خطا با سایر خطاهای مرتبط، پیشنهاد می‌کنم نوشته‌های خطای Object could not be converted to string در PHP وردپرس و خطای Cannot use object as array در PHP وردپرس را نیز مطالعه کنید.

خطای json_decode دقیقاً چه می‌گوید؟

خطای json_decode در PHP، برخلاف بسیاری از خطاهای PHP، یک خطای مرگبار Fatal Error نیست. تابع json_decode در صورت شکست، به‌جای پرتاب استثنا، مقدار null برمی‌گرداند و کد خطا را در یک متغیر داخلی ذخیره می‌کند. این طراحی، در نگاه اول ملایم به نظر می‌رسد ولی در عمل، منبع اصلی باگ‌های پنهان است چون کد شما بدون این‌که خطایی ببیند، به‌جای داده مورد انتظار، null دریافت می‌کند.

پیام دقیق این خطا در PHP معمولاً به‌شکل زیر ظاهر می‌شود، ولی فقط وقتی که شما صریحاً از json_last_error_msg() یا json_last_error() استفاده کنید:

$data = json_decode( $response_body );

if ( null === $data ) {
    echo json_last_error_msg();
    // Output: Syntax error
}

سه چیز در این مکانیزم مهم است. اول، تابع json_decode در صورت موفقیت مقدار را برمی‌گرداند و در صورت شکست مقدار null. دوم، این تابع هیچ Warning یا Notice تولید نمی‌کند، به همین دلیل کد شما بدون دخالت اضافه، خطا را نشان نمی‌دهد. سوم، کد خطا در یک متغیر داخلی ذخیره می‌شود و با توابع json_last_error و json_last_error_msg قابل خواندن است.

نکته مهم این است که در پروژه‌های واقعی، این خطا معمولاً به‌شکل دیگری ظاهر می‌شود. کد شما مقدار null را دریافت می‌کند، بدون بررسی، آن را در منطق بعدی به کار می‌برد و بعداً در جای دیگری از کد، خطای مبهم دیگری مثل «Cannot use object as array» یا «Object could not be converted to string» رخ می‌دهد. به همین دلیل، عیب‌یابی این خطا معمولاً به‌صورت غیرمستقیم و دشوار است.

json_decode در صورت شکست سکوت می‌کند و null برمی‌گرداند؛ این سکوت، منبع اصلی باگ‌های پنهان در پروژه‌های وردپرسی است.

JSON و نقش آن در وردپرس

JSON که در ادبیات فنی با نام کامل JavaScript Object Notation شناخته می‌شود، یک استاندارد باز برای تبادل داده است که از سال ۲۰۱۷ به‌عنوان RFC 8259 ثبت شده است. این استاندارد، امروز به رایج‌ترین فرمت تبادل داده در وب تبدیل شده و در اکثر APIهای مدرن، از Twitter تا GitHub و از Google تا AWS، به‌عنوان فرمت پیش‌فرض پاسخ به کار می‌رود.

JSON در وردپرس: نقش‌های کلیدی

در وردپرس، JSON نقشی محوری دارد که در چند سال اخیر با رشد REST API، اهمیتش چند برابر شده است. چهار کاربرد اصلی JSON در وردپرس وجود دارد که در پروژه‌های واقعی بارها دیده‌ام:

  1. REST API وردپرس: همه پاسخ‌های REST API وردپرس در فرمت JSON برگردانده می‌شوند. اگر با ساختار کلی REST API وردپرس آشنا نیستید، «REST API در وردپرس» مرور کاملی از این معماری دارد.
  2. ارتباط با سرویس‌های بیرونی: اکثر APIهای مدرن (پرداخت، پیامک، ایمیل، نقشه و…) پاسخ‌های JSON برمی‌گردانند.
  3. ذخیره داده ساختاریافته در Options: بعضی افزونه‌ها داده‌های پیچیده را به‌شکل JSON در Options API ذخیره می‌کنند.
  4. AJAX وردپرس: درخواست‌های AJAX اغلب با فرمت JSON پاسخ داده می‌شوند که در «آموزش افزونه فرم‌ساز وردپرس» نمونه‌های واقعی‌اش آمده است.

ساختار JSON و آناتومی یک خطای احتمالی

JSON یک ساختار متنی است که پنج نوع داده را پشتیبانی می‌کند: عدد، رشته، بولی، null، و ساختارهای ترکیبی مثل آرایه و آبجکت. برای درک دقیق خطاهای json_decode، باید این ساختار را بشناسید. جدول زیر خلاصه این انواع را نشان می‌دهد:

نوع JSONمثالمعادل PHP
عدد42int یا float
رشته"hello"string
بولیtruebool
nullnullnull
آرایه[1, 2, 3]array
آبجکت{"key": "value"}stdClass یا array

نکته مهم این است که در JSON، سه چیز مجاز نیست و اگر رعایت نشوند، خطای Syntax error رخ می‌دهد. اول، کاما در آخرین عضو آرایه یا آبجکت (trailing comma). دوم، کامنت درون JSON. سوم، استفاده از کوتِ ساده (single quote) به‌جای کوت دوتایی (double quote). این سه، شایع‌ترین علت خطا در پروژه‌های واقعی هستند.

فهرست کدهای خطای JSON و معنای دقیق هرکدام

تابع json_last_error() در PHP یک عدد برمی‌گرداند که هر عدد به یک نوع خطای خاص اشاره دارد. در بازبینی پروژه‌های واقعی، بیشتر خطاها به شش کد اصلی محدود می‌شوند که در ادامه هرکدام را باز می‌کنم.

کد 0: JSON_ERROR_NONE

این کد یعنی هیچ خطایی رخ نداده است. اگر json_decode مقدار null برگردانده ولی کد خطا 0 است، یعنی داده ورودی خودش null بوده (رشته JSON با محتوای "null")، نه این‌که خطایی رخ داده باشد. این تفکیک ظریف، منبع سردرگمی زیادی در پروژه‌های واقعی است.

کد 4: JSON_ERROR_SYNTAX

شایع‌ترین کد خطا. این خطا یعنی ساختار JSON نادرست است: کاما اضافه، کوت نامناسب، براکت بسته نشده، یا هر مشکل سینتکسی دیگر. در بیشتر سناریوها، این خطا به‌دلیل دستکاری داده یا پاسخ ناقص API مقصد رخ می‌دهد.

کد 5: JSON_ERROR_UTF8

این خطا یعنی داده ورودی شامل کاراکترهای نامعتبر UTF-8 است. در پروژه‌های ایرانی که با متن فارسی و کاراکترهای خاص سروکار دارند، این خطا شایع است. ریشه مشکل معمولاً در تبدیل Encoding نادرست در یکی از مراحل پردازش داده است.

کد 3: JSON_ERROR_CTRL_CHAR

این خطا یعنی داده ورودی شامل کاراکترهای کنترلی (control character) غیرمجاز است. کاراکترهای کنترلی مثل tab، newline و carriage return اگر درون رشته‌های JSON بدون escape کردن ظاهر شوند، این خطا را تولید می‌کنند. این خطا در پاسخ‌های API که به‌درستی escape نشده‌اند، شایع است.

کد 2: JSON_ERROR_DEPTH

این خطا یعنی ساختار JSON از عمق پیش‌فرض (معمولاً 512 سطح) عمیق‌تر است. در داده‌های پیچیده تودرتو یا در حملات DoS (Denial of Service) که با ساختار بسیار عمیق، سرور را فلج می‌کنند، این خطا رخ می‌دهد.

کد 1: JSON_ERROR_STATE_MISMATCH

این خطا یعنی ساختار JSON به‌طور غیرمنتظره‌ای ناقص است، مثل زمانی که در میانه یک آبجکت یا آرایه، داده قطع شود. این خطا معمولاً به‌دلیل قطع اتصال شبکه یا محدودیت اندازه پاسخ در سرور مقصد رخ می‌دهد.

جدول زیر خلاصه این کدها را با علت و راه‌حل مختصر نشان می‌دهد:

کدمعناعلت شایعراه‌حل
0No errorداده ورودی null استبررسی محتوای ورودی
2Depth errorساختار خیلی عمیقافزایش depth یا ساده‌سازی
3Control char errorکاراکتر کنترلیپاک‌سازی داده ورودی
4Syntax errorساختار نادرستاعتبارسنجی JSON
5UTF-8 errorکاراکتر نامعتبرتبدیل به UTF-8 معتبر
کدهای json_last_error، زبان تشخیص سریع این خطا هستند؛ اگر این کدها را بشناسید، از ساعت‌ها آزمون‌وخطا نجات پیدا می‌کنید.

ده علت ریشه‌ای در پروژه‌های وردپرسی

در بازبینی پروژه‌های واقعی، ده علت ریشه‌ای تکرارشونده برای خطای json_decode دیده‌ام. هر علت را با یک مثال واقعی و دلیل فنی توضیح می‌دهم تا در عیب‌یابی پروژه‌های خودتان بتوانید آن‌ها را تشخیص دهید.

علت اول: پاسخ HTML به‌جای JSON

شایع‌ترین علت. وقتی API مقصد به‌جای JSON، یک صفحه HTML برگرداند (مثلاً صفحه خطای 404 یا 500)، json_decode روی HTML شکست می‌خورد چون HTML ساختار JSON ندارد:

$response = wp_remote_get( $url );
$body = wp_remote_retrieve_body( $response );

// اگر body شامل HTML باشد، خطا رخ می‌دهد
$data = json_decode( $body, true );

// راه‌حل: بررسی پیش از decode
$code = wp_remote_retrieve_response_code( $response );

if ( 200 !== $code ) {
    return null;
}

$data = json_decode( $body, true );

راه‌حل، بررسی کد وضعیت HTTP قبل از decode و بررسی محتوای بدنه است. اصول دقیق در رفع خطای ارسال ایمیل در وردپرس در قالب مشابه با خطاهای ارسال آمده است.

علت دوم: BOM در ابتدای پاسخ

مشابه خطای headers already sent، اگر پاسخ API با یک BOM شروع شود، json_decode آن را به‌عنوان کاراکتر ناشناخته تلقی می‌کند و خطای Syntax error برمی‌گرداند. راه‌حل، حذف BOM از ابتدای رشته قبل از decode:

function myplugin_strip_bom( string $content ): string {
    $bom = pack( 'H*H*H*', 'EF', 'BB', 'BF' );

    if ( str_starts_with( $content, $bom ) ) {
        $content = substr( $content, 3 );
    }

    return $content;
}

$body = myplugin_strip_bom( wp_remote_retrieve_body( $response ) );
$data = json_decode( $body, true );

این الگو در همه پروژه‌هایی که با APIهای قدیمی سروکار دارند، کاربرد دارد. اصول کار با BOM در خطای headers already sent در PHP آمده است.

علت سوم: کاما اضافه در انتهای ساختار

کاما در آخرین عضو آرایه یا آبجکت در JSON مجاز نیست، ولی در JavaScript مجاز است. اگر داده‌ای که از یک منبع JavaScript می‌آید مستقیماً در PHP پردازش شود، این خطا رخ می‌دهد:

// اشتباه - کاما اضافه در انتهای آرایه
{"items": [1, 2, 3,]}

// درست
{"items": [1, 2, 3]}

راه‌حل، اصلاح داده در سطح منبع یا پاک‌سازی آن در سمت PHP است. روش پاک‌سازی با regex در موارد ضروری کاربرد دارد ولی رویکرد اصلی، اصلاح منبع داده است.

علت چهارم: کاراکترهای کنترلی در رشته‌ها

کاراکترهای کنترلی مثل newline، tab و carriage return اگر به‌درستی escape نشوند، خطای Control char را تولید می‌کنند. این وضعیت در پاسخ‌هایی که شامل متن چندخطی هستند، شایع است:

// اشتباه
{"message": "Line 1
Line 2"}

// درست
{"message": "Line 1\nLine 2"}

راه‌حل، escape کردن کاراکترهای کنترلی در سمت تولیدکننده داده است. اگر منبع داده در اختیار شما نیست، می‌توانید در سمت PHP، کاراکترهای کنترلی را حذف یا جایگزین کنید.

علت پنجم: کاراکترهای نامعتبر UTF-8

در پروژه‌های ایرانی که با متن فارسی، عربی یا سایر زبان‌ها سروکار دارند، این خطا شایع است. اگر داده‌ای از یک فایل با Encoding نادرست (مثل Windows-1256 یا ISO-8859-1) خوانده شود و به json_decode داده شود، خطای UTF-8 رخ می‌دهد. راه‌حل، تبدیل داده به UTF-8 معتبر قبل از decode:

function myplugin_ensure_utf8( string $content ): string {
    if ( ! mb_check_encoding( $content, 'UTF-8' ) ) {
        $detected = mb_detect_encoding( $content, [ 'UTF-8', 'Windows-1256', 'ISO-8859-1' ], true );

        if ( false !== $detected ) {
            $content = mb_convert_encoding( $content, 'UTF-8', $detected );
        }
    }

    return $content;
}

این الگو در همه پروژه‌های چندزبانه کاربرد دارد. اصول دقیق در آماده‌سازی قالب وردپرس برای زبان فارسی آمده است.

علت ششم: پاسخ خالی از API

اگر API مقصد پاسخ خالی برگرداند، json_decode روی رشته خالی خطا می‌دهد. این وضعیت در زمان قطع اتصال شبکه یا در زمان خطای سرور مقصد رخ می‌دهد:

$body = wp_remote_retrieve_body( $response );

if ( empty( $body ) ) {
    return null;
}

$data = json_decode( $body, true );

راه‌حل، بررسی خالی بودن بدنه قبل از decode است.

علت هفتم: محدودیت اندازه پاسخ

اگر پاسخ API بزرگ‌تر از محدودیت پاسخ PHP یا محدودیت حافظه سرور باشد، داده بریده می‌شود و json_decode خطای Syntax error برمی‌گرداند. راه‌حل، افزایش محدودیت‌ها یا پیاده‌سازی صفحه‌بندی در درخواست به API است. اصول دقیق در خطای Memory Limit در وردپرس آمده است.

علت هشتم: استفاده از کوت ساده

در JSON، کوت ساده مجاز نیست ولی در JavaScript و PHP رایج است. اگر داده‌ای با کوت ساده تولید شده باشد، json_decode خطای Syntax error برمی‌گرداند:

// اشتباه
{ 'name': 'Ali' }

// درست
{ "name": "Ali" }

راه‌حل، اصلاح داده در سمت منبع یا استفاده از کتابخانه‌های تخصصی برای تبدیل است.

علت نهم: پاسخ با Content-Type اشتباه

اگر API مقصد پاسخ را با Content-Type اشتباه برگرداند، json_decode به‌طور مستقیم خطا نمی‌دهد ولی کد شما ممکن است داده را به‌شکل نادرست تفسیر کند. راه‌حل، بررسی صریح Content-Type و اعتبارسنجی ساختار داده پس از decode است.

علت دهم: تزریق JavaScript در پاسخ

اگر داده JSON شامل JavaScript تزریق‌شده باشد (که در حملات JSON Hijacking رخ می‌دهد)، ممکن است خطای Syntax یا خطاهای ناخواسته رخ دهد. راه‌حل، استفاده از پارامتر JSON_HEX_TAG در زمان تولید JSON و اعتبارسنجی دقیق داده ورودی است.

در میان این ده علت، سه علت اول (پاسخ HTML، BOM و کاما اضافه) بیشترین سهم را در پروژه‌های واقعی دارند. اگر فقط این سه را در پروژه خود بررسی کنید، احتمالاً در ۷۰ درصد موارد به علت اصلی می‌رسید.

ده علت، یک ریشه مشترک دارند: داده ورودی، JSON معتبر نیست. رفع ریشه‌ای، اعتبارسنجی دقیق ورودی است نه تلاش برای دور زدن خطا.

مرحله تشخیص: از json_last_error تا لاگ کامل

تشخیص دقیق این خطا، اولین قدم رفع است. تجربه‌ام این است که اگر تشخیص درست انجام شود، رفع خطا معمولاً در چند دقیقه تمام می‌شود؛ ولی اگر تشخیص سطحی باشد، ساعت‌ها آزمون‌وخطا به همراه دارد. چهار ابزار اصلی در تشخیص این خطا استفاده می‌کنم.

ابزار اول: json_last_error و json_last_error_msg

اولین قدم، استفاده از توابع داخلی PHP برای دیدن کد خطای دقیق است:

$data = json_decode( $body, true );

if ( null === $data ) {
    $error_code = json_last_error();
    $error_msg  = json_last_error_msg();

    error_log( sprintf(
        '[MyPlugin] JSON decode failed - Code: %d, Message: %s',
        $error_code,
        $error_msg
    ) );
}

این الگو، کد خطا و پیام متنی آن را نشان می‌دهد که در تشخیص سریع کمک می‌کند. تابع json_last_error_msg از PHP 5.5 اضافه شده و پیام متنی قابل‌فهم‌تری ارائه می‌دهد.

ابزار دوم: لاگ بدنه پاسخ

برای تشخیص دقیق، گاهی نیاز است که خود بدنه پاسخ را لاگ کنید:

$body = wp_remote_retrieve_body( $response );

if ( strlen( $body ) > 10000 ) {
    $log_body = substr( $body, 0, 10000 ) . '... [truncated]';
} else {
    $log_body = $body;
}

error_log( '[MyPlugin] Response body: ' . $log_body );

توجه کنید که لاگ کامل بدنه در سایت production توصیه نمی‌شود چون اطلاعات حساس ممکن است در لاگ‌ها ثبت شوند. بهترین رویکرد، محدود کردن طول لاگ به ۱۰ کیلوبایت اول و شامل کردن بخش انتهایی در صورت نیاز است.

ابزار سوم: تست خط فرمان با cURL

برای تشخیص این‌که مشکل از کد PHP است یا از API مقصد، از ابزار خط فرمان cURL استفاده می‌کنم:

curl -v https://api.example.com/endpoint | head -50

این دستور، خروجی کامل را نمایش می‌دهد و در تشخیص کد وضعیت HTTP و محتوای پاسخ کمک می‌کند. اگر خط فرمان هم خطا برگرداند، مشکل در API مقصد است. اصول دقیق در خطای cURL در PHP آمده است.

ابزار چهارم: ابزارهای آنلاین اعتبارسنجی JSON

ابزارهای آنلاین مثل JSONLint می‌توانند خطای سینتکسی دقیق را در داده ورودی نشان دهند. توجه کنید که در استفاده از این ابزارها، هرگز داده‌های حساس سایت را در آن‌ها وارد نکنید چون به سرور شخص ثالث ارسال می‌شوند. برای داده‌های غیرحساس، این ابزارها تشخیص سریع و دقیقی فراهم می‌کنند.

نکته امنیتی در تشخیص

در سایت production، هرگز WP_DEBUG را برای مدت طولانی فعال نگه ندارید چون سه مشکل ایجاد می‌کند: اول، فایل debug.log می‌تواند به‌سرعت به چند گیگابایت برسد. دوم، اطلاعات حساس ممکن است در لاگ‌ها ثبت شوند. سوم، نمایش خطاها به کاربران، سطح امنیتی سایت را پایین می‌آورد. اصول کامل در نوشتن کد PHP امن برای وردپرس آمده است.

الگوهای رفع برای هر علت

حالا که علت‌ها و روش‌های تشخیص را می‌شناسید، به الگوهای رفع می‌رسیم. برای هر علت، راه‌حل مشخصی وجود دارد که در ادامه باز می‌کنم.

رفع با اعتبارسنجی پیش از decode

الگوی استاندارد، اعتبارسنجی داده پیش از decode است:

function myplugin_safe_json_decode( string $content ) {
    // بررسی خالی بودن
    if ( empty( $content ) ) {
        return new WP_Error( 'empty_content', 'Content is empty.' );
    }

    // حذف BOM
    $bom = pack( 'H*H*H*', 'EF', 'BB', 'BF' );
    if ( str_starts_with( $content, $bom ) ) {
        $content = substr( $content, 3 );
    }

    // اطمینان از UTF-8
    if ( ! mb_check_encoding( $content, 'UTF-8' ) ) {
        return new WP_Error( 'invalid_utf8', 'Invalid UTF-8 encoding.' );
    }

    $data = json_decode( $content, true );

    if ( null === $data ) {
        return new WP_Error(
            'json_decode_failed',
            json_last_error_msg()
        );
    }

    return $data;
}

این تابع، پنج لایه اعتبارسنجی دارد: خالی نبودن، حذف BOM، اطمینان از UTF-8، decode، و بررسی نتیجه. در همه پروژه‌های خودم، این تابع کمکی را در یک فایل جدا در پوشه includes نگه می‌دارم. اصول دقیق ساختار افزونه در ساختار استاندارد یک افزونه حرفه‌ای وردپرس آمده است.

رفع برای پاسخ HTML

برای رفع خطای پاسخ HTML، قبل از decode بررسی کنید که پاسخ واقعاً JSON است:

$response = wp_remote_get( $url );

if ( is_wp_error( $response ) ) {
    return null;
}

$code = wp_remote_retrieve_response_code( $response );
$content_type = wp_remote_retrieve_header( $response, 'content-type' );

if ( 200 !== $code || false === strpos( $content_type, 'application/json' ) ) {
    return null;
}

$data = json_decode( wp_remote_retrieve_body( $response ), true );

این الگو، دو لایه بررسی دارد: کد وضعیت HTTP و Content-Type. در همه پروژه‌های واقعی، این بررسی‌ها ضروری هستند.

رفع برای کاراکترهای کنترلی

برای کاراکترهای کنترلی، از پارامترهای json_decode استفاده کنید:

$data = json_decode( $content, true, 512, JSON_INVALID_UTF8_IGNORE );

پارامتر JSON_INVALID_UTF8_IGNORE کاراکترهای نامعتبر UTF-8 را نادیده می‌گیرد. در PHP 7.2 به بالا، این پارامتر در دسترس است. برای کاراکترهای کنترلی، پارامتر خاصی وجود ندارد ولی می‌توانید قبل از decode، کاراکترهای کنترلی را حذف کنید:

$content = preg_replace( '/\p{C}+/u', '', $content );

این regex، همه کاراکترهای کنترلی را حذف می‌کند. توجه کنید که این رویکرد ممکن است در بعضی سناریوها داده‌های مفید را هم حذف کند، پس با احتیاط استفاده شود.

رفع با پارامترهای json_decode

تابع json_decode در PHP چهار پارامتر دارد که هر کدام برای سناریوهای خاص مفید هستند:

$data = json_decode(
    $content,
    true,        // تبدیل آبجکت‌ها به آرایه
    512,         // عمق مجاز
    JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE
);

پارامتر JSON_THROW_ON_ERROR در PHP 7.3 به بالا در دسترس است و به‌جای برگرداندن null، یک استثنا پرتاب می‌کند:

try {
    $data = json_decode( $content, true, 512, JSON_THROW_ON_ERROR );
} catch ( JsonException $e ) {
    error_log( 'JSON decode failed: ' . $e->getMessage() );
    return null;
}

این رویکرد، مدیریت خطا را ساده‌تر می‌کند چون به‌جای بررسی null، می‌توانید در بلوک catch، خطا را مدیریت کنید.

رفع با تابع کمکی برای همه درخواست‌ها

در پروژه‌های بزرگ، یک تابع یکپارچه برای همه درخواست‌های HTTP که پاسخ JSON می‌دهند می‌سازم:

function myplugin_fetch_json( string $url, array $args = [] ) {
    $defaults = [
        'timeout'   => 15,
        'sslverify' => true,
        'headers'   => [ 'Accept' => 'application/json' ],
    ];

    $args = wp_parse_args( $args, $defaults );
    $response = wp_remote_get( $url, $args );

    if ( is_wp_error( $response ) ) {
        return $response;
    }

    $code = wp_remote_retrieve_response_code( $response );

    if ( 200 !== $code ) {
        return new WP_Error(
            'http_error',
            sprintf( 'HTTP %d returned', $code )
        );
    }

    $body = wp_remote_retrieve_body( $response );

    if ( empty( $body ) ) {
        return new WP_Error( 'empty_response', 'Response body is empty.' );
    }

    try {
        $data = json_decode( $body, true, 512, JSON_THROW_ON_ERROR );
    } catch ( JsonException $e ) {
        return new WP_Error( 'json_decode_failed', $e->getMessage() );
    }

    return $data;
}

این تابع، همه لایه‌های اعتبارسنجی و مدیریت خطا را در یک نقطه متمرکز می‌کند. در همه پروژه‌های خودم، این نوع توابع کمکی را در یک فایل جدا در پوشه includes نگه می‌دارم. اصول دقیق در JSON چیست و چطور داده‌ها را در وب ساختاردهی می‌کند؟ آمده است.

زمینه‌های خاص وردپرس: REST API و ذخیره متادیتا

در وردپرس، خطای json_decode در سه زمینه خاص بیشتر دیده می‌شود. در این بخش، هر زمینه را جداگانه بررسی می‌کنم.

زمینه اول: REST API وردپرس

در REST API وردپرس، همه پاسخ‌ها در فرمت JSON برگردانده می‌شوند. اگر در کدی که از REST API استفاده می‌کنید، داده پاسخ به‌درستی decode نشود، خطای مورد بحث رخ می‌دهد. الگوی استاندارد:

$response = wp_remote_get( rest_url( 'wp/v2/posts' ), [
    'headers' => [
        'Authorization' => 'Bearer ' . $token,
        'Accept'        => 'application/json',
    ],
] );

if ( is_wp_error( $response ) ) {
    return;
}

$body = wp_remote_retrieve_body( $response );
$posts = json_decode( $body, true );

if ( ! is_array( $posts ) ) {
    return;
}

نکته مهم در این الگو، تنظیم هدر Accept است که به وردپرس می‌گوید پاسخ را در فرمت JSON برگرداند. اصول دقیق کار با REST API در REST API در وردپرس و ساخت API اختصاصی برای وردپرس آمده است.

زمینه دوم: ذخیره داده در متادیتا

بعضی افزونه‌ها داده‌های پیچیده را به‌شکل JSON در User Meta یا Post Meta ذخیره می‌کنند. اگر این داده در زمان ذخیره‌سازی نامعتبر بوده یا در زمان خواندن به‌شکل نادرست decode شود، خطای مورد بحث رخ می‌دهد:

// ذخیره
$settings = wp_json_encode( $options );
update_user_meta( $user_id, 'my_settings', $settings );

// بازیابی
$settings_json = get_user_meta( $user_id, 'my_settings', true );
$settings = json_decode( $settings_json, true );

if ( ! is_array( $settings ) ) {
    $settings = [];
}

توصیه من این است که به‌جای ذخیره JSON در متادیتا، از خود آرایه PHP استفاده کنید چون وردپرس به‌طور خودکار آرایه را سریالایز و ذخیره می‌کند و در زمان خواندن، بدون نیاز به decode، آن را برمی‌گرداند. اصول دقیق در کار با User Meta در کدنویسی وردپرس و کار با Options API در کدنویسی وردپرس آمده است.

زمینه سوم: AJAX وردپرس

در درخواست‌های AJAX وردپرس، پاسخ‌ها اغلب با فرمت JSON برگردانده می‌شوند. الگوی استاندارد استفاده از توابع وردپرس:

function myplugin_ajax_handler() {
    check_ajax_referer( 'myplugin_ajax_action', 'nonce' );

    $data = myplugin_fetch_external_data();

    if ( is_wp_error( $data ) ) {
        wp_send_json_error( [
            'message' => $data->get_error_message(),
        ] );
    }

    wp_send_json_success( $data );
}
add_action( 'wp_ajax_myplugin_ajax', 'myplugin_ajax_handler' );

توابع wp_send_json_success و wp_send_json_error، خودشان داده را به JSON تبدیل می‌کنند و هدرهای مناسب را تنظیم می‌کنند. استفاده مستقیم از json_encode و header()، در اکثر سناریوها ضروری نیست. اصول کار با AJAX در امن‌سازی نشست‌های کاربری آمده است.

زمینه چهارم: WordPress Headless

در پروژه‌های WordPress Headless، فرانت‌اند از REST API وردپرس داده می‌گیرد و آن را در لایه نمایش استفاده می‌کند. اگر در این مسیر، داده به‌درستی decode نشود، خطای مورد بحث رخ می‌دهد. اصول دقیق در آموزش استفاده از REST API در وردپرس آمده است.

پیشگیری ساختاری در افزونه‌های سفارشی

بهترین راه‌حل برای خطای json_decode، پیشگیری است. در پروژه‌های واقعی، تجربه‌ام این است که اگر در پنج لایه پیشگیری انجام شود، این خطا تقریباً هرگز ظاهر نمی‌شود.

لایه اول: تابع کمکی برای decode امن

همیشه از یک تابع کمکی برای decode JSON استفاده کنید که همه لایه‌های اعتبارسنجی را داشته باشد:

function myplugin_safe_json_decode( string $content ) {
    if ( empty( $content ) ) {
        return new WP_Error( 'empty', 'Content is empty.' );
    }

    $bom = pack( 'H*H*H*', 'EF', 'BB', 'BF' );
    if ( str_starts_with( $content, $bom ) ) {
        $content = substr( $content, 3 );
    }

    try {
        return json_decode( $content, true, 512, JSON_THROW_ON_ERROR );
    } catch ( JsonException $e ) {
        return new WP_Error( 'json_error', $e->getMessage() );
    }
}

این تابع، پنج لایه امنیت دارد. در همه پروژه‌های خودم، این نوع توابع کمکی را در یک فایل جدا در پوشه includes نگه می‌دارم.

لایه دوم: بررسی Content-Type و کد وضعیت

قبل از decode، بررسی کنید که پاسخ واقعاً JSON است و کد وضعیت 2xx است:

$response = wp_remote_get( $url );

if ( is_wp_error( $response ) ) {
    return $response;
}

$code = wp_remote_retrieve_response_code( $response );

if ( $code < 200 || $code >= 300 ) {
    return new WP_Error( 'http_error', sprintf( 'HTTP %d', $code ) );
}

$content_type = wp_remote_retrieve_header( $response, 'content-type' );

if ( false === strpos( $content_type, 'application/json' ) ) {
    return new WP_Error( 'invalid_content_type', 'Not a JSON response.' );
}

این الگو، در همه پروژه‌های واقعی که با APIهای خارجی کار می‌کنند، توصیه می‌شود.

لایه سوم: استفاده از JSON_THROW_ON_ERROR

در PHP 7.3 به بالا، از پارامتر JSON_THROW_ON_ERROR استفاده کنید که به‌جای برگرداندن null، یک استثنا پرتاب می‌کند:

try {
    $data = json_decode( $content, true, 512, JSON_THROW_ON_ERROR );
} catch ( JsonException $e ) {
    error_log( 'JSON error: ' . $e->getMessage() );
    return null;
}

این رویکرد، مدیریت خطا را در همه سطوح ساده‌تر می‌کند.

لایه چهارم: استفاده از wp_json_encode برای encode

در سمت encode، همیشه از تابع wp_json_encode استفاده کنید نه json_encode:

$json = wp_json_encode( $data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );

تابع wp_json_encode خودش داده را اعتبارسنجی می‌کند و در صورت خطا، false برمی‌گرداند. پارامتر JSON_UNESCAPED_UNICODE باعث می‌شود کاراکترهای یونیکد (مثل فارسی) به‌شکل uXXXX کدگذاری نشوند و حجم خروجی کمتر شود. اصول دقیق در پاک‌سازی داده‌ها در کدنویسی وردپرس آمده است.

لایه پنجم: مستندسازی و لاگ‌گیری

در پروژه‌های حرفه‌ای، هر بار که json_decode شکست می‌خورد، لاگ کنید:

function myplugin_log_json_error( string $context, string $content ) {
    $log_data = [
        'context'   => $context,
        'error'     => json_last_error_msg(),
        'code'      => json_last_error(),
        'content'   => substr( $content, 0, 500 ),
        'timestamp' => current_time( 'mysql' ),
    ];

    error_log( '[MyPlugin JSON] ' . wp_json_encode( $log_data ) );
}

این الگو، در پروژه‌های بزرگ که چند API مختلف دارند، کاربرد دارد. با این لاگ‌ها، می‌توانید الگوهای تکرارشونده در خطاها را شناسایی کنید و ریشه مشکل را پیدا کنید.

لایه ششم: تست‌های واحد

نوشتن تست‌های واحد برای توابعی که با JSON کار می‌کنند، یکی از مؤثرترین راه‌های پیشگیری است. تست‌ها باید شامل سناریوهای زیر باشند: JSON معتبر، JSON ناقص، JSON با BOM، JSON با کاراکترهای کنترلی، پاسخ خالی. اگر با تست‌نویسی در وردپرس آشنایی کمتری دارید، تست و دیباگ پروژه‌های توسعه وردپرس مسیر را توضیح می‌دهد.

پیشگیری از خطای json_decode، نه با یک تکنیک بلکه با شش عادت کوچک محقق می‌شود؛ هرکدام به‌تنهایی کم‌اثر، ولی در کنار هم قوی.

پرسش‌های پرتکرار درباره خطای json_decode

خطای json_decode در PHP چه معنایی دارد؟ این خطا یعنی تابع json_decode نتوانسته رشته ورودی را به ساختار PHP تبدیل کند. تابع در صورت شکست، به‌جای پرتاب استثنا، مقدار null برمی‌گرداند و کد خطا را در یک متغیر داخلی ذخیره می‌کند که با json_last_error و json_last_error_msg قابل خواندن است.

تفاوت json_decode با json_encode چیست؟ json_decode رشته JSON را به ساختار PHP تبدیل می‌کند، درحالی‌که json_encode ساختار PHP را به رشته JSON تبدیل می‌کند. خطای json_decode معمولاً به‌دلیل داده نامعتبر ورودی رخ می‌دهد، درحالی‌که خطای json_encode نادرتر است و بیشتر به‌دلیل داده‌های غیرقابل تبدیل مثل منابع یا توابع رخ می‌دهد.

چرا json_decode خطا نمی‌دهد ولی null برمی‌گرداند؟ این طراحی PHP است که به‌جای پرتاب استثنا، مقدار null برگردانده شود. راه‌حل، بررسی نتیجه با توابع json_last_error و json_last_error_msg است. از PHP 7.3 به بالا، می‌توانید از پارامتر JSON_THROW_ON_ERROR استفاده کنید تا به‌جای null، استثنا پرتاب شود.

چطور بفهمم دقیقاً چه خطایی رخ داده؟ با استفاده از توابع json_last_error و json_last_error_msg که کد خطا و پیام متنی را برمی‌گردانند. جدول کدهای خطا در بخش «فهرست کدهای خطای JSON» همین مقاله آمده است. برای تشخیص دقیق‌تر، بدنه پاسخ را لاگ کنید.

تفاوت JSON_ERROR_SYNTAX با JSON_ERROR_UTF8 چیست؟ JSON_ERROR_SYNTAX یعنی ساختار JSON نادرست است (کاما اضافه، کوت اشتباه، براکت بسته نشده). JSON_ERROR_UTF8 یعنی داده ورودی شامل کاراکترهای نامعتبر UTF-8 است. ریشه این دو خطا متفاوت است و راه‌حل‌شان هم متفاوت است.

آیا json_decode در برابر حمله امنیتی مقاوم است؟ json_decode به‌خودی‌خود در برابر حملات رایج مقاوم است، ولی در سناریوهایی که داده کاربر مستقیماً به json_decode داده می‌شود، ریسک حمله DoS با ساختار عمیق یا بزرگ وجود دارد. راه‌حل، تعیین صریح پارامتر depth و بررسی اندازه داده قبل از decode است.

چرا json_decode روی داده‌ای که در JavaScript کار می‌کند، در PHP خطا می‌دهد؟ چون استاندارد JSON سختگیرانه‌تر از JavaScript است. مثلاً کاما در انتهای آرایه در JavaScript مجاز است ولی در JSON مجاز نیست. کوت ساده در JavaScript مجاز است ولی در JSON مجاز نیست. باید داده را با استاندارد JSON تولید کنید.

آیا استفاده از JSON_THROW_ON_ERROR توصیه می‌شود؟ بله، در PHP 7.3 به بالا این پارامتر توصیه می‌شود چون مدیریت خطا را در بلوک‌های try-catch ساده‌تر می‌کند و نیازی به بررسی صریح null نیست. در پروژه‌های جدید، استفاده از این پارامتر رویکرد بهتری است.

چطور در سرور بدون mb_check_encoding این خطا را تشخیص دهم؟ تابع mb_check_encoding بخشی از افزونه mbstring است که در اکثر سرورهای وردپرس فعال است. اگر این افزونه نصب نیست، می‌توانید از regex زیر برای بررسی UTF-8 استفاده کنید:

if ( ! preg_match( '//u', $content ) ) {
    // Invalid UTF-8
}

آیا خطای json_decode روی سرعت سایت اثر دارد؟ این خطا خودش روی سرعت اثر مستقیم ندارد، ولی اگر کد شما بدون بررسی null، درخواست‌های مکرر به API بیرونی بفرستد، این درخواست‌ها می‌تواند سرعت سایت را کاهش دهد. اصول دقیق در تاثیر هاست بر سرعت سایت چقدر است آمده است.

آیا این خطا در PHP 8 بیشتر از PHP 7.4 دیده می‌شود؟ فرکانس خطا تغییری نکرده، ولی در PHP 8، پیام‌های خطا دقیق‌تر شده‌اند. علاوه بر این، از PHP 7.3 پارامتر JSON_THROW_ON_ERROR اضافه شده که مدیریت خطا را ساده‌تر می‌کند.

آیا این خطا می‌تواند ناشی از افزونه وردپرس باشد؟ بله، در بازبینی‌ها زیاد دیده‌ام که این خطا از یک افزونه ثالث می‌آید که داده API بیرونی را بدون اعتبارسنجی decode می‌کند. راه‌حل، به‌روزرسانی افزونه یا جایگزینی آن با گزینه‌ای سازگارتر است.

چرا این خطا در زمان خواندن JSON از فایل رخ می‌دهد؟ اگر فایل JSON در زمان ذخیره‌سازی با Encoding نادرست (مثل Windows-1256) ذخیره شده باشد، json_decode خطای UTF-8 برمی‌گرداند. راه‌حل، ذخیره فایل با Encoding UTF-8 بدون BOM و بررسی محتوای فایل قبل از decode است.

آیا از کتابخانه‌های جایگزین json_decode می‌توان استفاده کرد؟ بله، در PHP چند کتابخانه جایگزین وجود دارد، ولی در اکثر پروژه‌ها نیازی به آن‌ها نیست. json_decode داخلی PHP برای اکثر سناریوها کافی است. استفاده از کتابخانه‌های جایگزین فقط در سناریوهای خاص مثل پشتیبانی از فرمت‌های غیراستاندارد توجیه دارد.

چرا این خطا در قالب‌های فارسی‌سازی‌شده بیشتر دیده می‌شود؟ این خطا به زبان فارسی مربوط نیست، ولی در پروژه‌هایی که با متن فارسی و APIهای محلی سروکار دارند، احتمال بروز مشکل UTF-8 بیشتر است. اصول دقیق در آماده‌سازی قالب وردپرس برای زبان فارسی آمده است.

چطور بفهمم JSON ورودی معتبر است یا نه؟ سه روش: اول، استفاده از ابزارهای آنلاین JSONLint برای داده‌های غیرحساس. دوم، استفاده از json_last_error_msg در کد PHP. سوم، لاگ کردن بدنه پاسخ و بررسی دستی آن. برای داده‌های حساس، فقط روش‌های دوم و سوم توصیه می‌شود.

آیا این خطا با خطای Cannot use object as array یکی است؟ نه، این دو خطا متفاوتند. خطای json_decode یعنی تبدیل رشته به ساختار PHP شکست خورده، درحالی‌که خطای Cannot use object as array یعنی کد شما به آبجکت با سینتکس آرایه دسترسی پیدا کرده. ریشه هر دو مشکل مشابه است — ناهماهنگی ساختار داده — ولی راه‌حل دقیق، متفاوت است. اگر با خطای دوم مواجه هستید، خطای Cannot use object as array در PHP وردپرس راهنمای مکملی است.

آیا می‌توانم پارامتر depth را در json_decode افزایش دهم؟ بله، پارامتر دوم json_decode به‌طور پیش‌فرض 512 است و می‌توانید آن را افزایش دهید. ولی توجه کنید که افزایش depth، ریسک حمله DoS را بالا می‌برد. بهترین رویکرد، افزایش تدریجی و استفاده از پارامترهای امنیتی در سرور است.

آیا این خطا با خطای Parse error در PHP یکی است؟ نه، این دو خطا متفاوتند. Parse error در زمان تجزیه کد PHP رخ می‌دهد، درحالی‌که خطای json_decode در زمان اجرا روی داده ورودی رخ می‌دهد. اگر با خطای اول مواجه هستید، خطای Parse error در PHP چیست و چگونه رفع می‌شود؟ راهنمای مکملی است.

از رفع موضعی به معماری مقاوم

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

سه اصل که در همه پروژه‌های خودم رعایت می‌کنم. اصل اول: هرگز json_decode را مستقیماً روی داده خارجی فراخوانی نکنید؛ همیشه از یک تابع کمکی استفاده کنید که همه لایه‌های اعتبارسنجی را داشته باشد. اصل دوم: در هر پروژه، یک تابع یکپارچه برای درخواست‌های HTTP که پاسخ JSON می‌دهند داشته باشید. اصل سوم: در PHP 7.3 به بالا از پارامتر JSON_THROW_ON_ERROR استفاده کنید که مدیریت خطا را ساده‌تر می‌کند. این سه اصل، در بلندمدت، خطاهای json_decode را به‌طور محسوس کاهش می‌دهند.

یک نکته عملی که در پروژه‌های واقعی زیاد به کارم آمده: پیش از انتشار افزونه‌ای که به API بیرونی وصل می‌شود، آن را با سه نوع داده تست کنید: JSON کامل و معتبر، JSON ناقص (بریده‌شده در میانه) و پاسخ HTML به‌جای JSON. اگر افزونه شما در هر سه سناریو، خطای مناسب برگرداند بدون این‌که سایت را با Fatal Error از کار بیندازد، یعنی معماری مدیریت خطا درست پیاده شده است.

اگر در ابتدای مسیر یادگیری هستید، سه تمرین را پیشنهاد می‌کنم. اول، روی یک نصب تستی وردپرس، عمداً یک رشته JSON ناقص را به json_decode بدهید و کدهای خطا را با json_last_error بررسی کنید. دوم، در محیط staging، یک درخواست به API واقعی بفرستید و بدنه پاسخ را لاگ کنید. سوم، در کد افزونه خود، یک تابع کمکی برای decode امن بنویسید و در همه جاهای کد از آن استفاده کنید. این سه تجربه، درک عمیقی از اهمیت مدیریت داده در cURL و JSON به شما می‌دهد که هیچ مقاله‌ای جایگزینش نمی‌شود. 🛠️