JSON en PHP: json_encode, json_decode y errores

JSON en PHP: json_encode convierte tus datos en texto JSON y json_decode hace lo contrario. Son dos funciones, pero con un par de trampas: los arrays y los objetos, las tildes, y sobre todo los errores, que por omisión no avisan.

De PHP a JSON: json_encode

<?php
declare(strict_types=1);

$pedido = [
    'id' => 1042,
    'cliente' => 'Ana López',
    'total' => 37.5,
    'pagado' => true,
    'productos' => ['café', 'taza'],
    'nota' => null,
];

echo json_encode($pedido), "\n\n";
echo json_encode($pedido, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION | JSON_THROW_ON_ERROR), "\n";

Probar en el editor →

salida{"id":1042,"cliente":"Ana L\u00f3pez","total":37.5,"pagado":true,"productos":["caf\u00e9","taza"],"nota":null}

{
    "id": 1042,
    "cliente": "Ana López",
    "total": 37.5,
    "pagado": true,
    "productos": [
        "café",
        "taza"
    ],
    "nota": null
}

json_encode convierte arrays, números, textos, booleanos y null en JSON. Un array con claves de texto se convierte en un objeto {}, y una lista (claves 0, 1, 2…) en un array []. Las opciones más útiles:

OpciónQué hace
JSON_PRETTY_PRINTsangrías y saltos de línea, para leerlo una persona
JSON_UNESCAPED_UNICODEdeja las tildes como están en vez de \u00e9
JSON_UNESCAPED_SLASHESno escapa las barras de las URL
JSON_PRESERVE_ZERO_FRACTIONescribe 37.0 en vez de 37 cuando el número es decimal
JSON_THROW_ON_ERRORlanza una JsonException si algo falla, en vez de devolver false

De JSON a PHP: json_decode

<?php
declare(strict_types=1);

$texto = '{"id": 1042, "cliente": "Ana López", "productos": ["café", "taza"], "envio": {"ciudad": "Lima"}}';

$comoArray = json_decode($texto, true, flags: JSON_THROW_ON_ERROR);
echo $comoArray['cliente'], ' vive en ', $comoArray['envio']['ciudad'], "\n";
echo count($comoArray['productos']), " productos\n";

$comoObjeto = json_decode($texto, flags: JSON_THROW_ON_ERROR);
echo $comoObjeto->cliente, ' vive en ', $comoObjeto->envio->ciudad, "\n";
echo get_debug_type($comoObjeto), "\n";

Probar en el editor →

salidaAna López vive en Lima
2 productos
Ana López vive en Lima
stdClass

El segundo argumento decide la forma del resultado. Con true, los objetos JSON se convierten en arrays asociativos, que es lo más cómodo y lo que verás en casi todo el código moderno. Sin él, en objetos stdClass, a los que se accede con ->. Fíjate en que flags: va con nombre: así no hace falta escribir el tercer argumento, la profundidad máxima.

Cuando el JSON está mal

<?php
declare(strict_types=1);

$roto = '{"cliente": "Ana", }';

var_dump(json_decode($roto, true));
echo json_last_error_msg(), "\n";

try {
    json_decode($roto, true, flags: JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
    echo "JsonException: ", $e->getMessage(), "\n";
}

var_dump(json_decode('null', true, flags: JSON_THROW_ON_ERROR));

Probar en el editor →

salidaNULL
Syntax error
JsonException: Syntax error
NULL

Sin opciones, un JSON roto devuelve null en silencio, y hay que preguntar a json_last_error_msg() qué pasó. Peor: el texto null también es JSON válido y también devuelve null, así que no hay forma de distinguirlos sin mirar el error. Con JSON_THROW_ON_ERROR el problema se convierte en una excepción y no hay confusión posible. Úsalo siempre.

Datos de fuera, siempre JSON. Para guardar o recibir datos de un formulario, una cookie o una API, usa JSON y no serialize/unserialize: JSON solo produce arrays, textos y números, mientras que unserialize puede crear objetos de tus clases. Lo explicamos en los fallos de seguridad típicos.

Guardar y leer un archivo JSON

<?php
declare(strict_types=1);

$ruta = sys_get_temp_dir() . '/config.json';

$config = ['tema' => 'oscuro', 'idioma' => 'es', 'avisos' => ['correo' => true, 'sms' => false]];
file_put_contents($ruta, json_encode($config, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));

$leida = json_decode(file_get_contents($ruta), true, flags: JSON_THROW_ON_ERROR);
echo "Tema: {$leida['tema']}\n";
echo "Avisos por correo: ", var_export($leida['avisos']['correo'], true), "\n";
echo filesize($ruta), " bytes en ", basename($ruta), "\n";

Probar en el editor →

salidaTema: oscuro
Avisos por correo: true
112 bytes en config.json

Resumen

ParaUsa
Convertir a JSONjson_encode($datos, JSON_THROW_ON_ERROR)
Leer JSON como arraysjson_decode($texto, true, flags: JSON_THROW_ON_ERROR)
JSON legible y con tildesJSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE
Detectar un JSON rotoJSON_THROW_ON_ERROR y catch (JsonException)
Responder JSON desde una webheader('Content-Type: application/json') (ver la API REST)

Todo el código de esta página se ejecutó con PHP 8.5.12 en un contenedor limpio, sin acceso a la red, antes de publicarla; las salidas y los mensajes de error están copiados de esa ejecución.