Eine API antwortet, die Daten sehen im Browser gut aus, und im Skript kommt trotzdem nichts an. Oder $daten['name'] löst einen Fehler aus, obwohl das Feld sichtbar im JSON steht. Beide Fälle haben mit PHP json_decode zu tun, genauer mit zwei Eigenheiten der Funktion, die man einmal verstanden haben muss.
Die erste betrifft den zweiten Parameter und ist schnell erledigt.
JSON einlesen mit PHP json_decode
Ohne zweiten Parameter liefert PHP json_decode ein Objekt der Klasse stdClass. Mit true liefert die Funktion ein assoziatives Array. Der Zugriff unterscheidet sich entsprechend.
<?php
$json = '{"name":"Kaffeemuehle","preis":49.90,"tags":["kueche","muehle"]}';
/* Ohne zweiten Parameter: stdClass */
$objekt = json_decode($json);
echo $objekt->name; /* Kaffeemuehle */
echo $objekt->tags[0]; /* kueche */
/* Mit true: assoziatives Array */
$array = json_decode($json, true);
echo $array['name']; /* Kaffeemuehle */
echo $array['tags'][0]; /* kueche */
Beachte den Unterschied bei tags: JSON-Arrays werden in beiden Fällen zu PHP-Arrays. Nur JSON-Objekte, also die Blöcke in geschweiften Klammern, hängen vom zweiten Parameter ab. In einer verschachtelten Struktur mischen sich deshalb Pfeil- und Klammerzugriff, wenn du ohne true arbeitest.
stdClass oder assoziatives Array?
Beide Varianten funktionieren. Die Wahl sollte trotzdem nicht dem Zufall überlassen bleiben, weil gemischte Verwendung im selben Projekt regelmässig zu Typfehlern beim Übergeben zwischen Funktionen führt.
| Sprich für | Array (mit true) | Objekt (Standard) |
| Schlüssel stehen erst zur Laufzeit fest | ja, $d[$name] ist selbstverständlich | möglich, aber umständlicher |
Array-Funktionen wie array_column() | direkt nutzbar | nur teilweise |
| Feste, bekannte Struktur | geht auch | ja, liest sich flüssiger |
| Schlüssel mit Bindestrich oder Leerzeichen | unproblematisch | nur über $o->{'mein-feld'} |
Die Empfehlung für neue Projekte lautet: true setzen und projektweit dabei bleiben. Der letzte Punkt der Tabelle gibt dabei den Ausschlag, denn Feldnamen mit Bindestrich kommen in fremden APIs häufig vor und machen den Objektzugriff sofort unhandlich.
Warum null zweideutig ist
Jetzt zur zweiten Eigenheit, und die ist die wichtigere. PHP json_decode liefert bei einem Fehler null zurück. Das Problem: null ist auch ein völlig gültiges Ergebnis, nämlich für die Eingabe "null". Am Rückgabewert allein lässt sich beides nicht unterscheiden.
<?php
var_dump(json_decode('null')); /* NULL, korrektes Ergebnis */
var_dump(json_decode('{kaputt')); /* NULL, Syntaxfehler */
/* Beide sehen gleich aus. Der Unterschied steht woanders: */
json_decode('null');
echo json_last_error(); /* 0, kein Fehler */
json_decode('{kaputt');
echo json_last_error(); /* 4 */
echo json_last_error_msg(); /* Syntax error */
Genau hier entstehen die stillen Ausfälle. Ein Skript verarbeitet monatelang Webhook-Daten, bis der Absender einmal etwas Ungültiges schickt. Ohne Prüfung läuft der Code mit null weiter, und der eigentliche Fehler taucht drei Funktionen später als unverständliche Meldung auf.
Ein Hinweis zu json_last_error(): die Funktion bezieht sich immer auf den zuletzt erfolgten Aufruf. Wer zwischen dem Dekodieren und der Fehlerprüfung ein zweites Mal dekodiert, prüft den falschen Vorgang.
Fehler abfangen mit JSON_THROW_ON_ERROR
Seit PHP 7.3 gibt es einen Weg, der die ganze Fehlerabfrage überflüssig macht. Das Flag JSON_THROW_ON_ERROR lässt PHP json_decode eine JsonException werfen, statt still null zu liefern.
<?php
try {
$daten = json_decode(
$eingabe,
true,
512,
JSON_THROW_ON_ERROR
);
} catch (JsonException $e) {
error_log('JSON ungueltig: ' . $e->getMessage());
http_response_code(400);
exit('Ungueltige Daten empfangen.');
}
/* Ab hier ist $daten garantiert dekodiert. */
Der Gewinn liegt nicht nur in der kürzeren Schreibweise. Eine Exception lässt sich nicht versehentlich übersehen, ein Rückgabewert schon. Für neuen Code ist das die richtige Variante, und die alte Fehlerabfrage bleibt nur dort nötig, wo PHP 7.2 oder älter unterstützt werden muss.
Beim Einlesen aus einer Datei kommt eine zweite Fehlerquelle dazu, die gern vergessen wird.
<?php
$pfad = __DIR__ . '/konfiguration.json';
$roh = file_get_contents($pfad);
if ($roh === false) {
throw new RuntimeException('Datei nicht lesbar: ' . $pfad);
}
$konfig = json_decode($roh, true, 512, JSON_THROW_ON_ERROR);
echo $konfig['datenbank']['host'];
Ohne die Prüfung auf false landet bei einer fehlenden Datei ein leerer String in der Funktion, und der ist kein gültiges JSON. Die Meldung lautet dann "Syntax error", was in die Irre führt: das eigentliche Problem war der Dateipfad.
Der folgende Ablauf zeigt beide Wege der Fehlerbehandlung nebeneinander.
flowchart TD
A[JSON-String] --> B[json_decode aufrufen]
B --> C{Flag gesetzt?}
C -->|JSON_THROW_ON_ERROR| D[Exception bei Fehler]
C -->|Ohne Flag| E{Rueckgabe null?}
E -->|Ja| F[json_last_error pruefen]
E -->|Nein| G[Daten vorhanden]
D --> G
G --> H[Struktur und Felder pruefen]
Verschachtelungstiefe mit dem depth-Parameter
Der dritte Parameter von PHP json_decode begrenzt, wie tief verschachtelt die Struktur sein darf. Der Standardwert ist 512, und wer ihn erreicht, bekommt eine Meldung, die ohne Vorwissen rätselhaft bleibt.
<?php
/* Sieben Ebenen tief, Limit auf 3 gesetzt */
$tief = '{"a":{"b":{"c":{"d":{"e":{"f":{"g":1}}}}}}}';
var_dump(json_decode($tief, true, 3));
echo json_last_error_msg(); /* Maximum stack depth exceeded */
var_dump(json_decode($tief, true, 20) !== null); /* bool(true) */
In der Praxis reichen 512 Ebenen fast immer aus. Wer den Wert erhöhen muss, sollte kurz innehalten: bei Daten aus fremder Quelle ist das Limit auch ein Schutz. Eine künstlich tief verschachtelte Eingabe kann sonst gezielt Speicher belegen.
Grosse Ganzzahlen und der Präzisionsverlust
Dieses Fehlerbild bleibt oft lange unbemerkt, weil nichts abbricht. Die Zahlen sind einfach falsch.
<?php
$json = '{"id":12345678901234567890}';
$standard = json_decode($json, true);
var_dump($standard['id']);
/* float(1.2345678901235E+19), Genauigkeit verloren */
$sicher = json_decode($json, true, 512, JSON_BIGINT_AS_STRING);
var_dump($sicher['id']);
/* string(20) "12345678901234567890" */
Sobald eine Ganzzahl grösser ist als PHP_INT_MAX, wandelt PHP json_decode sie in einen Float um, und Floats speichern nur eine begrenzte Anzahl signifikanter Stellen. Bei einer Bestell- oder Transaktionsnummer bedeutet das, dass die letzten Stellen verfälscht werden. Mit JSON_BIGINT_AS_STRING kommen solche Werte als String an und bleiben unverändert. Betroffen sind vor allem APIs, die IDs als Zahl statt als String liefern, was bei Snowflake-IDs und Zeitstempeln in Mikrosekunden häufig vorkommt.
JSON verlangt UTF-8
Die Spezifikation kennt nur UTF-8. Daten aus einem alten System in Latin-1 führen deshalb zu einem Fehler, dessen Meldung nicht auf die Ursache zeigt.
<?php
/* Ein Latin-1-kodierter Umlaut im JSON */
$latin = mb_convert_encoding('{"ort":"München"}', 'ISO-8859-1', 'UTF-8');
var_dump(json_decode($latin, true)); /* NULL */
echo json_last_error_msg(); /* Malformed UTF-8 characters ... */
/* Loesung: vorher konvertieren */
$utf8 = mb_convert_encoding($latin, 'UTF-8', 'ISO-8859-1');
var_dump(json_decode($utf8, true)); /* array mit korrektem Wert */
Prüfen lässt sich das mit mb_check_encoding($roh, 'UTF-8'), bevor überhaupt dekodiert wird. Das erspart die Suche nach einem vermeintlichen Syntaxfehler.
Gültiges JSON heisst nicht erwartete Struktur
Ein erfolgreicher Aufruf sagt nur, dass die Syntax stimmte. Ob die Felder da sind, mit denen dein Code rechnet, ist eine andere Frage, und die beantwortet PHP json_decode nicht.
<?php
$daten = json_decode($eingabe, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($daten)) {
throw new UnexpectedValueException('Objekt erwartet.');
}
foreach (['name', 'preis'] as $pflichtfeld) {
if (!array_key_exists($pflichtfeld, $daten)) {
throw new UnexpectedValueException(
'Feld fehlt: ' . $pflichtfeld
);
}
}
if (!is_numeric($daten['preis'])) {
throw new UnexpectedValueException('Preis muss eine Zahl sein.');
}
Die Prüfung auf is_array() am Anfang ist kein überflüssiger Schritt. Auch 42 und "text" sind gültiges JSON, und beides würde bei einem Feldzugriff sonst in einer schwer zu deutenden Meldung enden.
Wer nur wissen will, ob ein String gültiges JSON ist, ohne das Ergebnis zu brauchen, findet seit PHP 8.3 eine sparsamere Lösung im Tutorial zu PHP json_validate(). Die Gegenrichtung, also das Erzeugen von JSON aus PHP-Daten, behandelt das Tutorial zu PHP json_encode. Und wie eine Schnittstelle aussieht, die solche Daten entgegennimmt, zeigt PHP REST API erstellen.
Fazit zu PHP json_decode
Zwei Entscheidungen prägen jeden Aufruf von PHP json_decode. Die erste ist der zweite Parameter: true liefert Arrays und ist für die meisten Projekte die praktischere Wahl. Die zweite ist die Fehlerbehandlung, und dort führt in neuem Code kein Weg an JSON_THROW_ON_ERROR vorbei.
Drei Parameter lösen die Fehlerbilder, die sonst Stunden kosten. depth bei tief verschachtelten Daten, JSON_BIGINT_AS_STRING bei langen IDs, und eine Encoding-Prüfung bei Daten aus Altsystemen. Was danach bleibt, ist die Prüfung der Struktur, und die musst du selbst schreiben.