json_encode() Has Flags That Fix Its Worst Defaults
Run json_encode(['url' => 'https://example.com/café']) and look closely at what comes out:
{"url":"https:\/\/example.com\/café"}
Escaped slashes nobody asked for, unicode exploded into \u sequences. Both are legacy defaults (the slash-escaping guarded against </script> injection in inline HTML contexts; the unicode escaping catered to ancient latin-1 pipelines), and both make output larger and harder to read for no modern benefit.
The flags I consider mandatory
$json = json_encode($data,
JSON_THROW_ON_ERROR
| JSON_UNESCAPED_UNICODE
| JSON_UNESCAPED_SLASHES
);
JSON_THROW_ON_ERROR is the important one. Without it, json_encode returns false on failure (say, invalid UTF-8 from a scraped string or a NAN float) - and false quietly becomes the string "false" or an empty body somewhere downstream, failing far from the cause. With the flag, you get a JsonException at the actual crime scene. Same flag works on json_decode, where the silent-null failure mode has burned even more people.
UNESCAPED_UNICODE: "café" stays "café". Smaller payloads, readable logs, and correct behavior for every language that isn't ASCII.
UNESCAPED_SLASHES: URLs look like URLs. (If you're echoing JSON directly into an inline <script> block, keep slash escaping or - better - use JSON_HEX_TAG which escapes < and > specifically for that context.)
Situational flags worth knowing
- JSON_PRETTY_PRINT - for humans: config files, debug dumps, API explorer output. Never for production payloads (size).
- JSON_PRESERVE_ZERO_FRACTION - keeps 10.0 as 10.0 instead of 10. Matters when the consumer is typed (Java, Swift, strict JSON schema) and suddenly gets an int where it expected a double.
- JSON_PARTIAL_OUTPUT_ON_ERROR - the opposite philosophy to THROW: substitute nulls for unencodable values and keep going. Right choice in exactly one place: logging pipelines, where losing one field beats losing the log line.
- JSON_INVALID_UTF8_SUBSTITUTE - replaces broken byte sequences with U+FFFD instead of failing. Pair with scraped or user-supplied content.
Decode-side companions
$data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
// associative arrays, sane depth, loud failures
And for money or other precision-critical decimals: JSON_BIGINT_AS_STRING on decode keeps 64-bit ids from silently becoming floats.
Wrap your preferred flag combo in a two-line helper (json_out(), json_in()) and never think about it again - the same "fix the defaults once" move as letting URLSearchParams own query strings in JS. Defaults from 2006 shouldn't be making decisions in 2026.
Full-stack web developer sharing practical tutorials and building tools that ship.
Got something on your mind?
My inbox is open - no forms disappearing into the void here.
- Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
- Hire me for a project Have something custom in mind? Let's talk scope and timelines.
- Product support Bought something here? I'll help you get it running.
I usually reply within 1-2 business days.