PHP Array to JSON: json_encode Flags Explained
· by webmaster
json_encode() looks simple until an array comes out as an object, a URL fills up with backslashes, or a float grows fifteen extra digits. None of those are PHP bugs. In each case the function does exactly what it was told, and the fix is usually one flag or one array_values() call. Every sample below was run on PHP 8.5, and the output in the comments is what PHP printed.
If you only need to convert a static array once, paste it into the PHP Array to JSON converter. For the other direction, use JSON to PHP Array.
Associative vs indexed arrays: json_encode decides by the keys
PHP has one array type. JSON has two containers, objects ({}) and arrays ([]), so json_encode() has to pick one, and it decides by looking at the keys.
echo json_encode(['name' => 'Ada', 'age' => 36]);
// {"name":"Ada","age":36}
echo json_encode(['red', 'green']);
// ["red","green"]
An array becomes a JSON array only when its keys are integers, start at 0, and have no gaps. Everything else becomes an object. No flag exists to mark an array as a list.
Non-sequential keys turn a list into an object
Remove an element from a list and the sequence breaks. The output changes shape, and PHP gives no warning.
$list = ['a', 'b', 'c'];
unset($list[1]);
echo json_encode($list);
// {"0":"a","2":"c"}
echo json_encode(array_values($list));
// ["a","c"]
Keys that start at 1 do the same thing. When a front-end .map() fails on data that was supposed to be a list, this is the most likely cause. Call array_values() before encoding whenever the consumer expects an array.
Empty arrays always encode as []
An empty PHP array has no keys, so it always encodes as []. If your API promises an object, an empty result breaks any client that reads Object.keys(data) or data.items.length.
echo json_encode([]); // []
echo json_encode([], JSON_FORCE_OBJECT); // {}
JSON_FORCE_OBJECT applies to every array in the structure, nested lists included. For one empty map, cast to an object instead of using JSON_FORCE_OBJECT. json_encode((object) []) gives {} and leaves the rest of your data alone.
The json_encode flags, one by one
Flags are bit masks, and you combine them with |.
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
JSON_PRETTY_PRINT indents for human readers
This flag adds line breaks and four-space indentation. Use it for config files, logs and anything a person will read. Skip it for API responses, where the extra bytes add nothing. If you already have JSON and want it readable, the JSON Formatter does the same job in the browser and lets you pick two spaces or tabs.
echo json_encode(['a' => 1, 'b' => [1, 2]], JSON_PRETTY_PRINT);
{
"a": 1,
"b": [
1,
2
]
}
JSON_UNESCAPED_SLASHES keeps URLs readable
By default PHP escapes forward slashes as \/. That is valid JSON and every parser handles it, but it makes URLs harder to read and slightly larger.
echo json_encode(['url' => 'https://example.com/path']);
// {"url":"https:\/\/example.com\/path"}
echo json_encode(['url' => 'https://example.com/path'], JSON_UNESCAPED_SLASHES);
// {"url":"https://example.com/path"}
The default exists for one reason: JSON printed inside a <script> tag must not contain </, or the browser closes the tag early. If you are not printing JSON into HTML, turn it off.
JSON_UNESCAPED_UNICODE keeps UTF-8 characters
Without this flag, every non-ASCII character becomes a \uXXXX escape.
echo json_encode(['city' => 'Zürich']);
// {"city":"Z\u00fcrich"}
echo json_encode(['city' => 'Zürich'], JSON_UNESCAPED_UNICODE);
// {"city":"Zürich"}
Both forms decode to the same string. The unescaped one is smaller and readable, and modern APIs use UTF-8 anyway, so turn it on.
JSON_THROW_ON_ERROR throws instead of returning false
Since PHP 7.3, this flag makes json_encode() throw a JsonException instead of returning false. Add JSON_THROW_ON_ERROR to every encode and decode call. The error handling section below explains why.
JSON_FORCE_OBJECT turns every array into an object
Every array becomes an object, sequential lists included, as covered above. It is useful for maps that happen to be empty and harmful for real lists.
JSON_NUMERIC_CHECK damages numeric-looking strings
This flag converts strings that look like numbers into JSON numbers.
echo json_encode(['id' => '42', 'zip' => '01234'], JSON_NUMERIC_CHECK);
// {"id":42,"zip":1234}
The zip code lost its leading zero. Phone numbers, product codes and anything with a + sign are damaged the same way. The flag hides wrong types in your data, so fix the types instead.
JSON_PRESERVE_ZERO_FRACTION keeps 10.0 as a float
PHP drops .0 from floats, so 10.0 becomes 10. A strictly typed consumer (Go, Java, a JSON Schema validator) may then reject the value as an integer.
echo json_encode(['price' => 10.0]);
// {"price":10}
echo json_encode(['price' => 10.0], JSON_PRESERVE_ZERO_FRACTION);
// {"price":10.0}
JSON_PARTIAL_OUTPUT_ON_ERROR replaces bad values with null
This flag replaces values that cannot be encoded with null and continues, instead of failing the whole call. Treat it as a last resort for logging, where losing one field is better than losing the record.
$bad = ['ok' => 'fine', 'broken' => "\xB1\x31"]; // invalid UTF-8
var_dump(json_encode($bad));
// bool(false)
echo json_encode($bad, JSON_PARTIAL_OUTPUT_ON_ERROR);
// {"ok":"fine","broken":null}
echo json_encode($bad, JSON_INVALID_UTF8_SUBSTITUTE);
// {"ok":"fine","broken":"�1"}
JSON_INVALID_UTF8_SUBSTITUTE (PHP 7.2+) is the more targeted fix for bad byte sequences. It swaps each invalid byte for the replacement character and keeps the rest of the string, so you can still see what the value was.
json_encode flags quick reference
| Flag | Effect | Typical use |
|---|---|---|
JSON_PRETTY_PRINT | Indent with 4 spaces | Config files, debugging |
JSON_UNESCAPED_SLASHES | Keep / as is | URLs in API output |
JSON_UNESCAPED_UNICODE | Keep UTF-8 characters | Non-English content |
JSON_THROW_ON_ERROR | Throw JsonException | Everywhere |
JSON_FORCE_OBJECT | All arrays become {} | Empty maps |
JSON_NUMERIC_CHECK | Numeric strings become numbers | Rarely; risky |
JSON_PRESERVE_ZERO_FRACTION | Keep 10.0 as a float | Typed consumers |
JSON_PARTIAL_OUTPUT_ON_ERROR | Replace bad values with null | Logging |
JSON_INVALID_UTF8_SUBSTITUTE | Replace bad bytes with U+FFFD | User-supplied text |
Depth limits nesting to 512 levels by default
The third argument limits nesting. The default is 512, and a deeper structure fails with “Maximum stack depth exceeded”.
$deep = ['a' => ['b' => ['c' => 1]]];
var_dump(json_encode($deep, 0, 2));
// bool(false)
echo json_last_error_msg();
// Maximum stack depth exceeded
Recursive structures, such as a tree whose nodes point back at their parent, reach this limit quickly. Either break the cycle before encoding or implement JsonSerializable on the node class.
Floats and precision follow serialize_precision
PHP formats floats using the serialize_precision ini setting. Since PHP 7.1 the default is -1, meaning “the shortest string that round-trips to the same double”. That is the correct setting, but its output can look alarming:
echo json_encode(0.1 + 0.2);
// 0.30000000000000004
That value is correct. It is exactly what the addition produced in binary floating point, and any language that prints doubles at full precision prints the same thing. Older code sometimes pins serialize_precision to 17, which adds trailing noise to almost every float, or to 10, which rounds without telling you.
ini_set('serialize_precision', '10');
echo json_encode(0.1 + 0.2);
// 0.3
For money, encode integers in the smallest unit, never floats. Store and encode cents as integers, or encode a formatted string.
Objects encode public properties only, unless you use JsonSerializable
Plain objects encode their public properties and nothing else. PHP skips protected and private properties without a warning.
class User
{
public $name = 'Ada';
protected $email = 'ada@example.com';
private $hash = '...';
}
echo json_encode(new User);
// {"name":"Ada"}
To control the output, implement JsonSerializable. json_encode() calls jsonSerialize() and encodes whatever it returns.
class Money implements JsonSerializable
{
public function __construct(private int $cents, private string $currency) {}
public function jsonSerialize(): mixed
{
return ['amount' => $this->cents / 100, 'currency' => $this->currency];
}
}
echo json_encode(['total' => new Money(1999, 'EUR')], JSON_PRESERVE_ZERO_FRACTION);
// {"total":{"amount":19.99,"currency":"EUR"}}
JsonSerializable is also the clean way to hide fields, rename keys or format dates. DateTime objects encode to an object with date, timezone_type and timezone keys, which is rarely the format a client expects. Wrap them or call ->format(DATE_ATOM) first.
Decoding with json_decode returns objects unless you pass true
json_decode() returns stdClass objects by default. Pass true as the second argument to get associative arrays, which most PHP code finds easier to work with.
$json = '{"name":"Ada","tags":["a","b"],"meta":{"x":1}}';
$obj = json_decode($json);
$arr = json_decode($json, true);
echo $obj->meta->x; // 1
echo $arr['meta']['x']; // 1
Watch for two traps. Integers beyond PHP_INT_MAX become floats and lose precision, so pass JSON_BIGINT_AS_STRING as the fourth argument if you need them intact. Also, the literal null decodes to null, which is the same value a failed decode returns, so === null cannot tell you whether the parse failed. Use the error API or the throw flag.
var_dump(json_decode('{"id":12345678901234567890}', true));
// ["id"] => float(1.2345678901234567E+19)
var_dump(json_decode('{"id":12345678901234567890}', true, 512, JSON_BIGINT_AS_STRING));
// ["id"] => string(20) "12345678901234567890"
If the JSON you are decoding fails to parse, see Fix: JSON.parse Unexpected Token Errors. The causes are the same in every language.
Error handling: json_encode returns false without the throw flag
Without JSON_THROW_ON_ERROR, encode returns false and decode returns null on failure. Each sets an error code that you have to remember to check.
$result = json_decode('{"a":1', true);
if (json_last_error() !== JSON_ERROR_NONE) {
echo json_last_error_msg(); // Syntax error
}
That check is easy to forget. The throw flag makes failure impossible to ignore and puts the message and code on the exception.
try {
$json = json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
$back = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
// e.g. "Malformed UTF-8 characters, possibly incorrectly encoded" (code 5)
error_log($e->getMessage());
}
The encode error you are most likely to meet is JSON_ERROR_UTF8 (code 5). It usually comes from strings read out of a Latin-1 database without conversion. Fix the source encoding with mb_convert_encoding(), or use JSON_INVALID_UTF8_SUBSTITUTE for text you do not control.
As a project default, we recommend JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE on every encode, json_decode($x, true, 512, JSON_THROW_ON_ERROR) on every decode, array_values() after any unset(), and integers for money. To check a conversion without running PHP, drop the array into the PHP Array to JSON tool and compare it with your json_encode() output.