PHP · Advanced
JSON and Data Handling in PHP
In short: json_encode turns a PHP value into a JSON string and json_decode($text, true) turns JSON back into nested arrays. An array whose keys are 0..n-1 becomes a JSON array and any other array becomes a JSON object; JSON_THROW_ON_ERROR makes malformed input throw JsonException instead of returning null. Most data handling is reading text, splitting it into fields, converting them and building.
From PHP values to JSON and back
JSON has six kinds of value: object, array, string, number, boolean and null. PHP maps onto them almost directly. json_encode turns an int or float into a number, a string into a quoted string, true, false and null into their JSON spellings, and an array into either a JSON array or a JSON object. The rule for arrays is the one to remember: an array whose keys are exactly 0, 1, 2 and so on in order becomes a JSON array [...]; any other array, including one with string keys or a list with a gap, becomes a JSON object {...}. An empty array is a list, so it encodes as []; when the receiver expects {}, encode new stdClass or pass JSON_FORCE_OBJECT. Objects encode their public properties, and a class can implement JsonSerializable to return exactly the array it wants encoded.
Some defaults surprise people. Slashes are escaped as \/ and non-ASCII characters as \uXXXX; JSON_UNESCAPED_SLASHES and JSON_UNESCAPED_UNICODE turn that off. A float with no fractional part such as 20.0 encodes as 20; JSON_PRESERVE_ZERO_FRACTION keeps 20.0. JSON_PRETTY_PRINT indents with four spaces. Encoding fails, returning false, for invalid UTF-8 or for NAN and INF.
json_decode($text, true) parses JSON into nested PHP arrays; JSON objects become associative arrays and JSON arrays become lists. Without the true, objects become stdClass instances accessed with ->, which is less convenient for data work and is not what array functions expect. Numbers become int or float depending on whether they have a fraction or exponent; integers too large for 64 bits become floats unless JSON_BIGINT_AS_STRING is passed.
The classic trap is error handling. On malformed input json_decode returns null, but it also returns null for the valid document null, so testing the result is ambiguous. Pass JSON_THROW_ON_ERROR (PHP 7.3) as the fourth argument and malformed input throws JsonException with a message such as Syntax error; the second argument is the associative flag and the third is the maximum nesting depth, 512 by default. json_validate($text) (PHP 8.3) checks a string cheaply without building the result, useful when you only need yes or no.
Structured data rarely arrives as JSON in exercises; it arrives as lines. The pattern is always the same: read each line, skip blanks and comments, split it with explode, trim the fields, convert numbers with casts, and push an associative array onto a list. From there, array_column extracts a field, array_sum and count summarise, a $groups[$key][] = $row loop groups, and usort orders. The result is either printed with printf or encoded with json_encode, which is also the quickest way to inspect a nested array while debugging. stream_get_contents(STDIN) reads all remaining input at once, which is what you want when the whole input is one JSON document.
Syntax
$json = json_encode($value); // compact
$json = json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$json = json_encode($value, JSON_THROW_ON_ERROR); // throws JsonException instead of returning false
$data = json_decode($json, true); // nested arrays
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR); // throws on malformed input
$obj = json_decode($json); // stdClass objects, $obj->field
if (json_validate($json)) { ... } // PHP 8.3
$all = stream_get_contents(STDIN); // whole input as one string
[$a, $b, $c] = array_map("trim", explode(",", $line)); // split and clean one line
$rows[] = ["name" => $a, "qty" => (int) $b]; // build a record
$groups[$row["room"]][] = $row["name"]; // group by a field
array_column($rows, "qty"); array_sum(...); usort($rows, ...);Keys 0..n-1 in order encode as a JSON array; anything else as an object. An empty array is [] unless JSON_FORCE_OBJECT is used.
Encoding: what each PHP value becomes
A rain-gauge report shows the default mapping, then the effect of the escaping, zero-fraction and pretty-print flags.
<?php
$report = [
"station" => "Harbour Road",
"readings" => [3.5, 4, 2.75],
"units" => "mm",
"sensor_ok" => true,
"notes" => null,
"tags" => [],
];
echo json_encode($report), "\n";
$meta = ["path" => "/data/2024", "name" => "Ørsted"];
echo json_encode($meta), "\n";
echo json_encode($meta, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), "\n";
echo json_encode(["a", "b"]), " ", json_encode([1 => "a", 2 => "b"]), " ", json_encode(new stdClass), "\n";
echo json_encode(["temp" => 20.0]), " ", json_encode(["temp" => 20.0], JSON_PRESERVE_ZERO_FRACTION), "\n";
echo json_encode(["k" => [1, 2]], JSON_PRETTY_PRINT), "\n";Output
{"station":"Harbour Road","readings":[3.5,4,2.75],"units":"mm","sensor_ok":true,"notes":null,"tags":[]}
{"path":"\/data\/2024","name":"\u00d8rsted"}
{"path":"/data/2024","name":"Ørsted"}
["a","b"] {"1":"a","2":"b"} {}
{"temp":20} {"temp":20.0}
{
"k": [
1,
2
]
}The report's string keys make it an object; readings has keys 0, 1, 2 and becomes an array; the empty tags becomes []. Slashes and the Ø are escaped by default and left alone with the two flags. [1 => "a", 2 => "b"] is not a list because it does not start at 0, so it becomes an object with string keys, and new stdClass is the way to get {}. The float 20.0 loses its fraction unless asked to keep it. Pretty printing indents by four spaces and puts every element on its own line.
Decoding a JSON order from input and summarising it
The whole input is one JSON document; it is decoded with errors turned into exceptions, walked, and summarised back to JSON.
<?php
$json = stream_get_contents(STDIN);
try {
$order = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
echo "bad JSON: ", $e->getMessage(), "\n";
exit(1);
}
$total = 0;
$byCategory = [];
foreach ($order["lines"] as $line) {
$cost = $line["qty"] * $line["unit_price"];
$total += $cost;
$byCategory[$line["category"]] = ($byCategory[$line["category"]] ?? 0) + $line["qty"];
}
echo "Order ", $order["id"], " for ", $order["customer"]["name"], "\n";
echo "Total: ", number_format($total, 2), "\n";
ksort($byCategory);
echo json_encode([
"order" => $order["id"],
"items_by_category" => $byCategory,
"total" => round($total, 2),
]), "\n";Input given to the program: {"id": 5081, "customer": {"name": "Tomasz", "vip": false}, "lines": [{"sku": "P-14", "category": "paint", "qty": 3, "unit_price": 7.5}, {"sku": "B-2", "category": "brushes", "qty": 5, "unit_price": 1.2}, {"sku": "P-9", "category": "paint", "qty": 1, "unit_price": 12}]}
Output
Order 5081 for Tomasz
Total: 40.50
{"order":5081,"items_by_category":{"brushes":5,"paint":4},"total":40.5}stream_get_contents slurps the entire input because a JSON document may span many lines. With true the nested objects become associative arrays, so $order["customer"]["name"] is ordinary array access. The numbers keep their types: qty is an int, unit_price a float, and the arithmetic needs no casts. The category tally is sorted by key before encoding so the output is deterministic, and the summary is emitted as JSON that another program could consume.
From plain lines to a nested structure
Room bookings arrive as comma-separated lines with a comment and a blank line; they become records, are grouped by room, and are printed as pretty JSON.
<?php
$rows = [];
while (($line = fgets(STDIN)) !== false) {
$line = trim($line);
if ($line === "" || str_starts_with($line, "#")) {
continue;
}
[$name, $room, $hours] = array_map("trim", explode(",", $line));
$rows[] = ["name" => $name, "room" => $room, "hours" => (float) $hours];
}
$byRoom = [];
foreach ($rows as $row) {
$byRoom[$row["room"]][] = $row["name"];
}
$totalHours = array_sum(array_column($rows, "hours"));
echo json_encode([
"bookings" => count($rows),
"hours" => $totalHours,
"rooms" => $byRoom,
], JSON_PRETTY_PRINT), "\n";
var_dump(json_validate('{"ok": true}'), json_validate("{'ok': true}"));Input given to the program: # name, room, hours ↵ Aoife, Studio 2, 1.5 ↵ Ben, Lab, 3 ↵ ↵ Chen, Studio 2, 2
Output
{
"bookings": 3,
"hours": 6.5,
"rooms": {
"Studio 2": [
"Aoife",
"Chen"
],
"Lab": [
"Ben"
]
}
}
bool(true)
bool(false)The reading loop is defensive: blank lines and the # header are skipped, and array_map("trim", ...) cleans every field in one call before the three-way destructuring. Grouping is a single line: $byRoom[$room][] = $name creates the inner list on first use. rooms encodes as an object because its keys are room names, while each inner list encodes as an array. The last line shows json_validate accepting real JSON and rejecting single-quoted keys, which JSON does not allow.
Common mistakes
Detecting decode errors by comparing the result with null
Why it goes wrong:
json_decode("null")legitimately returnsnull, and so does malformed input, so the check cannot tell them apart and older code that usedjson_last_error()is easy to forget.Fix: Pass
JSON_THROW_ON_ERRORand catchJsonException.PHP · fix<?php try { $data = json_decode($text, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $e) { echo "invalid: ", $e->getMessage(), "\n"; }Forgetting the second argument to json_decode
Why it goes wrong: Without
true, objects becomestdClassand$data["id"]throwsError: Cannot use object of type stdClass as array.Fix: Use
json_decode($text, true)for data you will treat as arrays, or access fields with->if you keep objects.Sending [] where the consumer expects {}
Why it goes wrong: An empty PHP array is a list, so a map that happens to be empty encodes as
[], and a strict client rejects it as the wrong type.Fix: Encode
new stdClassfor an empty object, or convert with(object) $map, or useJSON_FORCE_OBJECTwhen every array should be an object.Ignoring the return value of json_encode
Why it goes wrong: It returns
falsefor invalid UTF-8 or non-finite floats, andecho falseprints nothing, so the output silently disappears.Fix: Pass
JSON_THROW_ON_ERRORtojson_encodeas well, or checkjson_last_error_msg()when the result isfalse.
PHP value to JSON
| PHP | JSON | Note |
|---|---|---|
| int, float | number | 20.0 becomes 20 without JSON_PRESERVE_ZERO_FRACTION |
| string | string | slashes and non-ASCII escaped by default |
| true / false / null | true / false / null | |
| list array (keys 0..n-1) | array | [] when empty |
| any other array | object | keys become strings |
| object | object of public properties | or what jsonSerialize() returns |
| stdClass with no properties | {} | the way to emit an empty object |
Where you use this
Configuration files, API responses, message queues and logs are JSON, so a PHP program that talks to anything outside itself decodes on the way in and encodes on the way out. A command-line report tool reads a JSON export, groups and totals it with the array functions, and writes JSON for the next tool or a table for a person. In exercises the input is usually plain lines, but the middle of the program is identical: build a list of associative arrays, then summarise, and json_encode is the fastest way to check that the structure you built is the one you meant.
$config = json_decode(file_get_contents("settings.json"), true, 512, JSON_THROW_ON_ERROR);
$port = $config["port"] ?? 8080;
file_put_contents("summary.json", json_encode($summary, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));Key points
json_encode($value)returns a string;json_decode($text, true)returns nested arrays.- Keys 0..n-1 in order encode as a JSON array; everything else as an object; empty arrays as
[]. - Use
JSON_THROW_ON_ERRORand catchJsonExceptionrather than testing for null or false. JSON_PRETTY_PRINT,JSON_UNESCAPED_SLASHES,JSON_UNESCAPED_UNICODEandJSON_PRESERVE_ZERO_FRACTIONcontrol the output.- Implement
JsonSerializableto control how an object encodes. json_validate(PHP 8.3) checks syntax without decoding.- Line-based data: skip blanks,
explode,trim, cast, build records, then group with$g[$k][] = $vand summarise witharray_columnandarray_sum.
Try it yourself
The input is a JSON array of numbers. The program decodes it and prints how many there are. Change it to print a JSON object with the keys min, max and sum, in that order.
<?php
$numbers = json_decode(stream_get_contents(STDIN), true, 512, JSON_THROW_ON_ERROR);
echo count($numbers), "\n";
[4, 9, 1, 6]{"min":1,"max":9,"sum":20}<?php
$numbers = json_decode(stream_get_contents(STDIN), true, 512, JSON_THROW_ON_ERROR);
echo json_encode([
"min" => min($numbers),
"max" => max($numbers),
"sum" => array_sum($numbers),
]), "\n";
Practise this
Related lessons
- Associative Arrays and Sorting in PHPUse string keys to model records in PHP, check keys safely, tally and group data, and sort by value, key or a custom rule with sort, asort, ksort and usort.10 min
- Strings in PHPSingle and double quotes, interpolation and heredoc, the string functions you reach for daily, multibyte text with mb_ functions and sprintf formatting in PHP.10 min
- Exceptions and Error Handling in PHPThrow and catch PHP exceptions, write custom exception classes, use finally for cleanup, tell Error from Exception, and turn warnings into exceptions with.11 min
Frequently asked questions
How do I convert JSON to a PHP array?
Call json_decode($text, true). The second argument makes JSON objects come back as associative arrays instead of stdClass objects, so the whole document is nested arrays you can index with []. Add JSON_THROW_ON_ERROR as the fourth argument so malformed input throws JsonException instead of returning null.
Why does json_encode output {} for some arrays and [] for others?
It looks at the keys. An array whose keys are exactly 0, 1, 2, ... in order is a list and becomes a JSON array. An array with string keys, or integer keys that are out of order or have gaps, becomes a JSON object. After unset on a list the keys have a gap, so call array_values before encoding if you want an array. An empty array is a list and gives [].
How do I detect json_decode errors in PHP?
Pass JSON_THROW_ON_ERROR as the fourth argument and catch JsonException; its message says what went wrong, such as Syntax error or Maximum stack depth exceeded. The older approach is to call json_last_error() after decoding and compare with JSON_ERROR_NONE, which still works but is easy to forget. json_validate (PHP 8.3) returns a boolean without decoding at all.
How this page was checked. Every program on it was run with PHP 8.4 at build time by the publishing checks, and the output shown is what it printed. Running PHP inside the browser is not available yet, so the Run button is absent rather than pretending; copy the code and run it with PHP 8.4 locally.