Debugging an API on the Fly: Why Is Your JSON Returning with an Error?
\n\nHave you ever sat at three in the morning, staring at a blank Postman screen, thinking: "Why is my PHP script returning HTML instead of JSON?"? If so — you are not alone. The topic Returning JSON from a PHP Script on Stack Overflow has gathered tens of thousands of views, and for good reason. Every second beginner backend developer steps on the same rake: forgotten headers, garbage output, incorrect Content-Type.
\n\nIn this article, we won't just copy a response from a forum. We will break down how to return JSON from PHP correctly, quickly, and without surprises. You will learn why exit(json_encode($data)) can let you down, how to save milliseconds on serialization, and what to do if JSON suddenly "breaks".
Ready? Let's go.
\n\nAnatomy of a Proper JSON Response
\n\nLet's start with the basics. Here's how not to do it:
\n\n<?php\n$data = ['status' => 'ok', 'message' => 'Hello!'];\necho json_encode($data);\n?>\n\nWhy is this bad? First, there is no Content-Type header. The browser or client may interpret the response as HTML or text. Second, if there was any whitespace, echo, or warning in the code before this, the JSON will break.
The correct minimal template looks like this:
\n\n<?php\nheader('Content-Type: application/json; charset=utf-8');\n$data = ['status' => 'ok', 'message' => 'Hello!'];\necho json_encode($data, JSON_UNESCAPED_UNICODE);\nexit;\n?>\n\nNote the JSON_UNESCAPED_UNICODE — it keeps Cyrillic readable instead of turning it into \\u043F\\u0440\\u0438\\u0432\\u0435\\u0442. And exit — a safeguard against accidental output after the response.
Headers: Not Just Content-Type
\n\nFor JSON performance optimization, add a couple more headers:
\n\nheader('Content-Type: application/json; charset=utf-8');\nheader('X-Content-Type-Options: nosniff');\nheader('Cache-Control: no-cache, must-revalidate');\n\nX-Content-Type-Options: nosniff prevents the browser from "guessing" the content type — this is for security. And Cache-Control is useful for dynamic data so the client doesn't cache an outdated response.
Error Handling: When JSON_ENCODE Fails
\n\nThe json_encode function returns false on failure. And this is where things get interesting. If you simply do echo json_encode($data) and get an empty string in response — the client will be left puzzled.
Always check the result:
\n\n$json = json_encode($data, JSON_UNESCAPED_UNICODE);\nif ($json === false) {\n http_response_code(500);\n echo json_encode(['error' => 'JSON encoding failed: ' . json_last_error_msg()]);\n exit;\n}\necho $json;\nexit;\n\nBy the way, json_last_error_msg() will return a clear string: "Malformed UTF-8 characters" or "Inf and NaN cannot be JSON encoded". This will save hours of debugging.
Deeply Nested Data and Recursion
\n\nAnother problem is circular references in objects. If you pass an object with mutual references, json_encode will throw an error. Solution: use JSON_PARTIAL_OUTPUT_ON_ERROR or pre-convert the object to an array.
$json = json_encode($complexObject, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR);\n\nBut be careful: partial output may be incomplete. It's better to fix the data structure.
\n\nOptimizing JSON Performance in PHP
\n\nNow let's move to the most interesting part — how to speed up returning JSON from PHP.
\n\n1. Caching Serialized JSON
\n\nIf the data changes rarely (directories, configs, city lists), don't serialize them every time. Save the ready JSON to a file, Memcached