PHP · Advanced

JSON and Data Handling in PHP

11 min readUpdated September 24, 2026Every example verified

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

 PHP · 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
<?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
<?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
<?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, hoursAoife, Studio 2, 1.5Ben, Lab, 3Chen, 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 returns null, and so does malformed input, so the check cannot tell them apart and older code that used json_last_error() is easy to forget.

    Fix: Pass JSON_THROW_ON_ERROR and catch JsonException.

     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 become stdClass and $data["id"] throws Error: 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 stdClass for an empty object, or convert with (object) $map, or use JSON_FORCE_OBJECT when every array should be an object.

  • Ignoring the return value of json_encode

    Why it goes wrong: It returns false for invalid UTF-8 or non-finite floats, and echo false prints nothing, so the output silently disappears.

    Fix: Pass JSON_THROW_ON_ERROR to json_encode as well, or check json_last_error_msg() when the result is false.

PHP value to JSON

PHPJSONNote
int, floatnumber20.0 becomes 20 without JSON_PRESERVE_ZERO_FRACTION
stringstringslashes and non-ASCII escaped by default
true / false / nulltrue / false / null
list array (keys 0..n-1)array[] when empty
any other arrayobjectkeys become strings
objectobject of public propertiesor 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.

 PHP · in practice
$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_ERROR and catch JsonException rather than testing for null or false.
  • JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES, JSON_UNESCAPED_UNICODE and JSON_PRESERVE_ZERO_FRACTION control the output.
  • Implement JsonSerializable to 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][] = $v and summarise with array_column and array_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.

Your program
<?php
$numbers = json_decode(stream_get_contents(STDIN), true, 512, JSON_THROW_ON_ERROR);
echo count($numbers), "\n";
Input the program receives: [4, 9, 1, 6]
Expected output: {"min":1,"max":9,"sum":20}

Practise this

Open the PHP playground

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.

Progress is stored only in this browser.

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.