PHP JSON

JSON é a língua que os programas usam para trocar dados. Quando um aplicativo de celular pede a lista de produtos a um site, quando o site consulta o CEP num serviço dos Correios, quando você salva as configurações de um programa: quase sempre os dados viajam em JSON. O PHP transforma qualquer array em JSON com uma função, e qualquer JSON de volta em dados com outra. Nesta aula você vai usar as duas, com as bandeiras que deixam o resultado legível, e vai aprender a não se enganar com um JSON quebrado.

Você vai aprender
  • O que é JSON e como ele se parece com os arrays do PHP.
  • json_encode: array → texto JSON, com JSON_PRETTY_PRINT e JSON_UNESCAPED_UNICODE (sem ela, ç vira ç).
  • json_decode: texto JSON → dados, como objeto ou como array (com true).
  • Descobrir que um JSON está errado: json_last_error_msg, JSON_THROW_ON_ERROR e json_validate (PHP 8.3+).
  • Guardar e ler dados num arquivo .json.

Para esta aula, você precisa conhecer arrays associativos e ler e escrever arquivos.


A ideia: uma ficha que qualquer programa entende

JSON (JavaScript Object Notation, "notação de objetos do JavaScript", pronuncia-se "jêison") nasceu no JavaScript, mas hoje toda linguagem lê e escreve JSON. É texto puro, com poucas regras. Veja a ficha de um produto da lojinha da Ana, em JSON:

{
    "nome": "Pão de mel",
    "preco": 4.5,
    "estoque": 30,
    "vegano": false,
    "sabores": ["chocolate", "doce de leite"],
    "foto": null
}

E a mesma ficha como array do PHP:

$produto = [
    'nome'    => 'Pão de mel',
    'preco'   => 4.5,
    'estoque' => 30,
    'vegano'  => false,
    'sabores' => ['chocolate', 'doce de leite'],
    'foto'    => null,
];
JSONPHPRegra do JSON
{ "chave": valor, … } (objeto)array associativo ['chave' => valor]Chaves sempre entre aspas duplas.
[ valor, valor ] (lista)array indexado [valor, valor]Sem vírgula depois do último item.
"texto"stringSó aspas duplas; aspas simples não valem.
4.5, 30float, intDecimal com ponto.
true, falseboolMinúsculas.
nullnullMinúsculas.

Transformar dados em texto para guardar ou enviar tem um nome: serializar (ou "codificar", encode). O caminho de volta é desserializar ("decodificar", decode). Daí os nomes das funções.


json_encode: do PHP para o JSON

<?php
$produto = [
    'nome'    => 'Pão de mel',
    'preco'   => 4.5,
    'estoque' => 30,
    'vegano'  => false,
    'sabores' => ['chocolate', 'doce de leite'],
    'foto'    => null,
];
echo json_encode($produto), "\n";
$ cd json
$ php enc1.php
{"nome":"Pão de mel","preco":4.5,"estoque":30,"vegano":false,"sabores":["chocolate","doce de leite"],"foto":null}

Funcionou: é JSON válido, numa linha só, sem espaços (o formato mais econômico para enviar pela rede). Mas repare no nome: Pão. Por padrão, o json_encode troca toda letra fora do inglês básico por um código de escape \uXXXX (ã é o "ã"). Qualquer programa que ler esse JSON vai entender "Pão" direitinho, mas para gente é ilegível:

$ php enc3.php      « três json_encode sem e com bandeiras
{"cidade":"São José","site":"https:\/\/loja.example\/bolos"}
{"cidade":"São José","site":"https://loja.example/bolos"}
["açúcar","maçã"]

Na última linha, açúcar e maçã viraram açúcar e maçã: o ç é ç. E as barras de endereço viraram \/. A segunda linha foi gerada com duas bandeiras (flags) que desligam esses escapes.

As bandeiras mais úteis

O segundo argumento do json_encode aceita bandeiras, combinadas com | (como as do file()):

<?php
// … o mesmo $produto …
echo json_encode($produto, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), "\n";
$ php enc2.php
{
    "nome": "Pão de mel",
    "preco": 4.5,
    "estoque": 30,
    "vegano": false,
    "sabores": [
        "chocolate",
        "doce de leite"
    ],
    "foto": null
}
BandeiraTraduçãoEfeito
JSON_PRETTY_PRINT"impressão bonita"Quebra linhas e recua com 4 espaços. Ótimo para arquivos que pessoas vão ler.
JSON_UNESCAPED_UNICODE"Unicode sem escape"Mantém ã, ç, é como são (em UTF-8), em vez de ã.
JSON_UNESCAPED_SLASHES"barras sem escape"Mantém / em vez de \/.
JSON_THROW_ON_ERROR"lançar exceção no erro"Se der errado, lança uma exceção em vez de devolver false (veja adiante).
JSON_FORCE_OBJECT"forçar objeto"Gera {} mesmo para listas e arrays vazios.
JSON_PRESERVE_ZERO_FRACTION"manter o .0"Grava 10.0 como 10.0, e não 10.

Missão: rode e veja os ç e companhia. Depois acrescente as bandeiras para o JSON sair em várias linhas e com os acentos legíveis ("nome": "Açúcar mascavo").

<?php
$item = [
    'nome'       => 'Açúcar mascavo',
    'quantidade' => 2,
    'unidade'    => 'kg',
    'fornecedor' => ['cidade' => 'São José', 'contato' => null],
];
echo json_encode($item), "\n";
<?php
$item = [
    'nome'       => 'Açúcar mascavo',
    'quantidade' => 2,
    'unidade'    => 'kg',
    'fornecedor' => ['cidade' => 'São José', 'contato' => null],
];
echo json_encode($item, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), "\n";

Lista ou objeto? E outros detalhes

O PHP tem um tipo só (o array) para duas coisas que no JSON são diferentes (lista [] e objeto {}). Ele decide assim: se as chaves forem 0, 1, 2…, em ordem e sem buracos, vira lista; senão, vira objeto.

<?php
echo json_encode([10, 10.0, 0.1 + 0.2, '42', true, null]), "\n";
echo json_encode([]), "\n";
echo json_encode(['a', 'b', 'c']), "\n";
echo json_encode([1 => 'a', 2 => 'b']), "\n";
$lista = ['a', 'b', 'c'];
unset($lista[1]);
echo json_encode($lista), "\n";
echo json_encode(array_values($lista)), "\n";
echo json_encode([], JSON_FORCE_OBJECT), "\n";
echo json_encode('Ela disse "oi"'), "\n";
$ php tipos.php
[10,10,0.30000000000000004,"42",true,null]      « 10.0 virou 10; '42' continua texto
[]
["a","b","c"]                                   « chaves 0,1,2: lista
{"1":"a","2":"b"}                               « começa em 1: objeto
{"0":"a","2":"c"}                               « o unset abriu um buraco: objeto!
["a","c"]                                       « array_values renumera: lista de novo
{}
"Ela disse \"oi\""                              « aspas dentro do texto são escapadas

A quinta linha é uma armadilha comum: depois de remover um item com unset (ou filtrar com array_filter), as chaves ficam 0, 2, e quem esperava uma lista recebe um objeto. Passe por array_values antes de codificar.

E objetos de classes?

json_encode também aceita objetos: ele grava as propriedades públicas e ignora as privadas.

$ php objeto.php     « classe Produto com $nome e $preco públicos e $custo privado
{"nome":"Brigadeiro","preco":2.5}

Para escolher o que vai no JSON, a classe pode implementar a interface JsonSerializable. Você vai ver classes e interfaces no Nível 3 (Classes e Objetos, Interfaces).


json_decode: do JSON para o PHP

O caminho de volta. json_decode recebe o texto JSON e devolve os dados. Mas atenção ao formato que ele escolhe por padrão:

<?php
$json = '{"nome":"Pão de mel","preco":4.5,"sabores":["chocolate","doce de leite"]}';
$obj = json_decode($json);
var_dump($obj);
echo $obj->nome, "\n";
echo $obj->sabores[0], "\n";
$ php dec1.php
object(stdClass)#1 (3) {               « um OBJETO, não um array
  ["nome"]=>
  string(11) "Pão de mel"
  ["preco"]=>
  float(4.5)
  ["sabores"]=>
  array(2) {                           « as listas viram arrays
    [0]=>
    string(9) "chocolate"
    [1]=>
    string(13) "doce de leite"
  }
}
Pão de mel
chocolate

Sem o segundo argumento, cada objeto JSON {…} vira um objeto da classe genérica stdClass ("classe padrão"), e você lê os campos com a seta ->: $obj->nome. Passando true como segundo argumento ("associativo"), tudo vira array, e você usa colchetes como sempre:

<?php
$json = '{"nome":"Pão de mel","preco":4.5,"sabores":["chocolate","doce de leite"]}';
$arr = json_decode($json, true);
var_dump($arr);
echo $arr['nome'], "\n";
echo $arr['sabores'][1], "\n";
$ php dec2.php
array(3) {                             « agora um array associativo
  ["nome"]=>
  string(11) "Pão de mel"
  ["preco"]=>
  float(4.5)
  ["sabores"]=>
  array(2) {
    [0]=>
    string(9) "chocolate"
    [1]=>
    string(13) "doce de leite"
  }
}
Pão de mel
doce de leite
json_decode($json)json_decode($json, true)
Objeto JSON viraobjeto stdClassarray associativo
Ler um campo$d->nome$d['nome']
Funções de array (count, foreach com chave, array_map…)só nas listasem tudo
Quando usarGosto pessoal; alguns frameworks preferemRecomendado para começar: é o que você já conhece

Neste guia, vamos usar true quase sempre. Misturar os dois estilos é o erro mais comum com JSON:

$ php dec-erro-obj.php      « $obj = json_decode('{"nome":"Pão de mel"}'); echo $obj['nome'];

Fatal error: Uncaught Error: Cannot use object of type stdClass as array in /home/ana/json/dec-erro-obj.php:3
Stack trace:
#0 {main}
  thrown in /home/ana/json/dec-erro-obj.php on line 3
$ php dec-erro-arr.php      « $arr = json_decode('{"nome":"Pão de mel"}', true); echo $arr->nome;

Warning: Attempt to read property "nome" on array in /home/ana/json/dec-erro-arr.php on line 3

Missão: um pedido chegou do site em JSON. Decodifique-o como array e mostre: Cliente: Bia, uma linha por item no formato 2 x Bolo de cenoura, e o total (quantidade × preco somados) como Total: R$ 82,00.

<?php
$json = '{
  "cliente": "Bia",
  "itens": [
    {"produto": "Bolo de cenoura", "quantidade": 2, "preco": 35},
    {"produto": "Brigadeiro", "quantidade": 4, "preco": 3}
  ]
}';

$pedido = json_decode($json);
var_dump($pedido);
<?php
$json = '{
  "cliente": "Bia",
  "itens": [
    {"produto": "Bolo de cenoura", "quantidade": 2, "preco": 35},
    {"produto": "Brigadeiro", "quantidade": 4, "preco": 3}
  ]
}';

$pedido = json_decode($json, true);
echo "Cliente: {$pedido['cliente']}\n";
$total = 0;
foreach ($pedido['itens'] as $item) {
    echo "{$item['quantidade']} x {$item['produto']}\n";
    $total += $item['quantidade'] * $item['preco'];
}
echo "Total: R$ " . number_format($total, 2, ',', '.') . "\n";

Quando o JSON está errado

JSON é rigoroso: uma vírgula a mais ou uma aspa simples e o texto inteiro deixa de ser JSON. E o pior: json_decode não mostra aviso nenhum. Ele só devolve null, em silêncio. Para saber o que houve, pergunte com json_last_error() (um número) e json_last_error_msg() (a mensagem):

<?php
$textos = [
    "{'nome': 'Ana'}",
    '{"nome": "Ana",}',
    '{"nome": "Ana"',
    '',
    'null',
];
foreach ($textos as $t) {
    $r = json_decode($t, true);
    echo str_pad($t, 18), " => ";
    var_dump($r);
    echo "   erro: ", json_last_error(), " - ", json_last_error_msg(), "\n";
}
$ php invalido.php
{'nome': 'Ana'}    => NULL                « aspas simples: proibidas no JSON
   erro: 4 - Syntax error
{"nome": "Ana",}   => NULL                « vírgula depois do último item
   erro: 4 - Syntax error
{"nome": "Ana"     => NULL                « faltou fechar a chave
   erro: 4 - Syntax error
                   => NULL                « texto vazio também não é JSON
   erro: 4 - Syntax error
null               => NULL                « este é JSON válido: o valor null!
   erro: 0 - No error

A última linha mostra o problema de confiar no null: o texto null é um JSON perfeito, cujo valor é null. Só o json_last_error() (0 = "nenhum erro") separa os dois casos. Syntax error = "erro de sintaxe": o texto não segue as regras.

Melhor: JSON_THROW_ON_ERROR

Ficar perguntando json_last_error() depois de cada chamada é fácil de esquecer. Com a bandeira JSON_THROW_ON_ERROR, um JSON inválido vira uma exceção (JsonException), que você captura com try/catch (aula Exceções):

<?php
$json = '{"nome": "Ana",}';
try {
    $dados = json_decode($json, true, flags: JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
    echo "JSON inválido: ", $e->getMessage(), "\n";
}
json_decode($json, true, flags: JSON_THROW_ON_ERROR)
As bandeiras são o quarto argumento (o terceiro é a profundidade máxima, 512 por padrão). Com o argumento nomeado flags: você pula o terceiro. A forma sem nome é json_decode($json, true, 512, JSON_THROW_ON_ERROR).
try { … }
"Tente": se algo lá dentro lançar uma exceção, o PHP pula para o catch.
catch (JsonException $e)
"Capture" exceções do tipo JsonException; $e->getMessage() traz a mensagem.
$ php throw.php
JSON inválido: Syntax error

E sem o try, a exceção para o programa, com uma mensagem clara (o que é bem melhor do que seguir com null e quebrar dez linhas depois):

$ php throw2.php     « json_decode('{"nome": "Ana",}', true, 512, JSON_THROW_ON_ERROR);

Fatal error: Uncaught JsonException: Syntax error in /home/ana/json/throw2.php:2
Stack trace:
#0 /home/ana/json/throw2.php(2): json_decode('{"nome": "Ana",...', true, 512, 4194304)
#1 {main}
  thrown in /home/ana/json/throw2.php on line 2

(O 4194304 no rastro é o valor numérico da bandeira JSON_THROW_ON_ERROR.) A mesma bandeira funciona no json_encode.

Só conferir: json_validate PHP 8.3+

Quando você só quer saber se o texto é JSON válido, sem usar os dados, json_validate responde true ou false, gastando menos memória que o json_decode:

<?php
var_dump(json_validate('{"nome": "Ana"}'));
var_dump(json_validate('{"nome": "Ana",}'));
echo json_last_error_msg(), "\n";
$ php validate.php
bool(true)
bool(false)
Syntax error

Em versões antigas (antes do 8.3), use json_decode e confira json_last_error() === JSON_ERROR_NONE.

Erro no json_encode: texto que não é UTF-8

O JSON exige UTF-8. Se uma string veio de um arquivo antigo, gravado em outra codificação (Latin-1, comum em sistemas do Windows), o json_encode falha:

<?php
$ruim = "P\xE3o";      // "Pão" em Latin-1: o ã é o byte E3
var_dump(json_encode(['nome' => $ruim]));
echo json_last_error_msg(), "\n";
var_dump(json_encode(['nome' => $ruim], JSON_INVALID_UTF8_SUBSTITUTE));
$ php utf8.php
bool(false)
Malformed UTF-8 characters, possibly incorrectly encoded
string(19) "{"nome":"P�o"}"         « o byte ruim virou o caractere de substituição

"Caracteres UTF-8 malformados, possivelmente codificados de forma incorreta". O conserto de verdade é converter o texto para UTF-8 antes (mb_convert_encoding($ruim, 'UTF-8', 'ISO-8859-1'), veja mbstring).

Missão: o programa lê dois pedidos, e o segundo está quebrado. Hoje ele mostra um Warning confuso. Use JSON_THROW_ON_ERROR com try/catch (JsonException $e) para que o segundo mostre JSON inválido: Syntax error e nenhum aviso apareça.

<?php
$pedidos = [
    '{"cliente": "Caio", "itens": 3}',
    "{'cliente': 'Duda', 'itens': 1}",
];
foreach ($pedidos as $json) {
    $p = json_decode($json, true);
    echo "Pedido de {$p['cliente']} com {$p['itens']} itens\n";
}
<?php
$pedidos = [
    '{"cliente": "Caio", "itens": 3}',
    "{'cliente': 'Duda', 'itens': 1}",
];
foreach ($pedidos as $json) {
    try {
        $p = json_decode($json, true, flags: JSON_THROW_ON_ERROR);
        echo "Pedido de {$p['cliente']} com {$p['itens']} itens\n";
    } catch (JsonException $e) {
        echo "JSON inválido: ", $e->getMessage(), "\n";
    }
}

JSON num arquivo

Juntando com a aula de arquivos: um arquivo .json é um ótimo "banquinho de dados" para coisas pequenas (configurações, uma lista de produtos, um cache). O ciclo é sempre o mesmo: ler o arquivo e decodificar, mudar o array, codificar e gravar.

<?php
$arquivo = __DIR__ . '/produtos.json';
$produtos = [
    ['nome' => 'Bolo de cenoura', 'preco' => 35.0],
    ['nome' => 'Torta de limão', 'preco' => 48.5],
];
file_put_contents($arquivo, json_encode($produtos, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));

$lidos = json_decode(file_get_contents($arquivo), true);
$lidos[] = ['nome' => 'Brigadeiro', 'preco' => 2.5];
file_put_contents($arquivo, json_encode($lidos, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));

foreach (json_decode(file_get_contents($arquivo), true) as $p) {
    echo $p['nome'], ': R$ ', number_format($p['preco'], 2, ',', '.'), "\n";
}
$ php arquivo.php
Bolo de cenoura: R$ 35,00
Torta de limão: R$ 48,50
Brigadeiro: R$ 2,50
$ cat produtos.json
[
    {
        "nome": "Bolo de cenoura",
        "preco": 35                       « o 35.0 foi gravado como 35
    },
    {
        "nome": "Torta de limão",
        "preco": 48.5
    },
    {
        "nome": "Brigadeiro",
        "preco": 2.5
    }
]$                                        « sem quebra de linha no fim do arquivo

Dois detalhes reais: o 35.0 virou 35 (ao ler de volta, ele chega como int; use JSON_PRESERVE_ZERO_FRACTION se isso importar), e o json_encode não põe quebra de linha no final, por isso o prompt do terminal grudou no ]. Se quiser, grave json_encode(…) . "\n".

Arquivo JSON × banco de dados

Para poucos dados, lidos e gravados por uma pessoa por vez, um arquivo JSON resolve. Quando muita gente grava ao mesmo tempo, ou os dados crescem, use um banco de dados (veja Banco de Dados: o Básico). E para enviar JSON como resposta de um site (uma API), você vai precisar do cabeçalho Content-Type: application/json; isso fica para a aula Criar uma API JSON.


Quando dá errado

json_decode devolveu NULL

O que significa: o texto não é JSON válido (aspas simples, vírgula sobrando, chave sem aspas, texto vazio porque o arquivo não foi lido…), ou o JSON era literalmente null.

Como consertar: mostre json_last_error_msg() logo depois, ou use JSON_THROW_ON_ERROR. Para achar o erro no texto, cole o JSON num validador ou confira com json_validate.

"Cannot use object of type stdClass as array"
Fatal error: Uncaught Error: Cannot use object of type stdClass as array in /home/ana/json/dec-erro-obj.php:3

O que significa: você decodificou sem true (objeto) e tentou ler com colchetes (array).

Como consertar: json_decode($json, true), ou leia com $obj->campo. O contrário (-> num array) dá Warning: Attempt to read property "nome" on array.

"Malformed UTF-8 characters, possibly incorrectly encoded"

O que significa: o json_encode devolveu false porque alguma string não está em UTF-8.

Como consertar: converta a origem para UTF-8 com mb_convert_encoding. Como paliativo, JSON_INVALID_UTF8_SUBSTITUTE troca os bytes ruins por � (o losango com interrogação).

Esperava uma lista […] e veio um objeto {"0":…,"2":…}

O que significa: as chaves do array não são 0, 1, 2… seguidas (depois de unset ou array_filter).

Como consertar: json_encode(array_values($lista)).


Referência

Função / bandeiraO que faz
json_encode($dados, $flags)Dados → texto JSON (ou false se falhar).
json_decode($json, true)Texto JSON → arrays (sem true: objetos stdClass); null se inválido.
json_decode($json, true, 512, $flags)Com profundidade máxima e bandeiras.
json_last_error() / json_last_error_msg()Código (0 = sem erro) e mensagem do último erro.
json_validate($json) 8.3+true/false: é JSON válido?
JSON_PRETTY_PRINTVárias linhas, recuado.
JSON_UNESCAPED_UNICODEAcentos legíveis (sem ç).
JSON_UNESCAPED_SLASHES/ sem \/.
JSON_THROW_ON_ERRORErro vira JsonException.
JSON_PRESERVE_ZERO_FRACTION10.0 continua 10.0.
JSON_FORCE_OBJECTSempre {}.
JSON_INVALID_UTF8_SUBSTITUTETroca bytes inválidos por �.

Exercícios

1. Acentos legíveis

Complete para que maçã não vire maçã:

echo json_encode($frutas, );

2. Array em vez de objeto

Complete para receber arrays associativos:

$dados = json_decode($json, );

3. A mensagem do erro

Complete para mostrar por que o último json_decode falhou:

echo ;

4. Qual destes textos é JSON válido?

5. $l = ['a', 'b', 'c']; unset($l[0]); O que json_encode($l) mostra?

6. O que json_decode('{"x": 1,}') faz?

Missão final: o estoque.json (segunda aba) guarda os produtos. Leia o arquivo como array, diminua em 1 o estoque do Bolo de cenoura, mostre Vendido: Bolo de cenoura, grave o arquivo de volta (bonito e com acentos legíveis) e mostre o conteúdo novo. O bolo deve ficar com "estoque": 4.

<?php
$arquivo = __DIR__ . '/estoque.json';

[
    {"nome": "Bolo de cenoura", "estoque": 5},
    {"nome": "Torta de limão", "estoque": 2}
]
<?php
$arquivo = __DIR__ . '/estoque.json';

$produtos = json_decode(file_get_contents($arquivo), true, flags: JSON_THROW_ON_ERROR);
foreach ($produtos as $i => $p) {
    if ($p['nome'] === 'Bolo de cenoura') {
        $produtos[$i]['estoque']--;
        echo "Vendido: {$p['nome']}\n";
    }
}
file_put_contents($arquivo, json_encode($produtos, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n");
echo file_get_contents($arquivo);

Resumo

  • JSON é texto com objetos {}, listas [], textos em aspas duplas, números com ponto, true/false/null.
  • json_encode transforma arrays em JSON; use JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE para gente ler (sem a segunda, ç vira ç).
  • json_decode($json, true) devolve arrays; sem true, objetos stdClass (lidos com ->).
  • JSON inválido não avisa: json_decode devolve null. Use JSON_THROW_ON_ERROR (ou json_last_error_msg()); para só conferir, json_validate (8.3+).
  • Arquivo JSON: file_get_contents + json_decode para ler, json_encode + file_put_contents para gravar.
  • Listas com buracos viram objetos: use array_values.

Próxima aula: Datas e Horas.