Três fronteiras de validação
Num serviço fictício de operações, o pedido de criação de tarefa exige um objeto com batchId textual e attempts inteiro entre zero e cinco. Primeiro verifica se o corpo é JSON válido; depois se a raiz é um objeto; finalmente se os campos cumprem o contrato. Um documento null é JSON válido mas não representa esse pedido. Um attempts negativo pode ter o tipo certo e violar o intervalo. Constrói uma tabela de entradas com objeto válido, array, campo ausente, null explícito e número fora do intervalo. Decide o resultado esperado antes da implementação. Esta separação ajuda o suporte a distinguir um problema de transporte de uma regra de negócio rejeitada.
Identidade antes de conveniência
O identificador 00042 pertence a um sistema externo e os zeros fazem parte da identidade. Mantém esse valor como string; convertê-lo para inteiro e voltar a texto não recupera a representação original. Para um inteiro JSON superior à capacidade de PHP, JSON_BIGINT_AS_STRING evita convertê-lo primeiro num float aproximado. Isso não resolve sozinho diferenças entre consumidores: um browser pode ter limites diferentes, pelo que um contrato de IDs textuais é mais claro. Uma lista de tarefas tem outro problema: depois de remover elementos, as chaves podem deixar de ser consecutivas. Reindexa apenas se forem posições sem significado. Se as chaves forem IDs de tarefas, conserva-as num formato de objeto declarado.
Resultados e erros pertencem à mesma chamada
JSON_THROW_ON_ERROR permite tratar sintaxe inválida sem confundir o erro com o valor JSON null. Usa um modelo consistente por operação: capturar JsonException ou verificar o estado da chamada sem essa flag. Misturar modelos pode fazer o suporte ler um erro global antigo depois de uma operação bem-sucedida. Se precisares dos campos, descodifica uma vez; json_validate antes de json_decode repete a análise. json_validate existe desde PHP 8.3 e é útil quando apenas interessa a validade sintática. Para respostas, decide previamente se bytes UTF-8 inválidos devem provocar rejeição ou substituição documentada. Uma política de rejeição não deve ativar saída parcial, porque mudaria a garantia prometida ao consumidor.
Capturar sem esconder defeitos
Uma falha de formato recebida do cliente e um TypeError causado pelo programa pedem diagnósticos diferentes. JsonException é uma Exception; TypeError pertence à família Error. Throwable abrange ambas, mas capturar tudo e devolver sucesso apagaria essa distinção. Num adaptador HTTP, traduz erros de entrada esperados para uma resposta estável e conserva um identificador de correlação para investigação. Não exponhas o corpo completo nem detalhes internos só para facilitar o diagnóstico. Coloca catches específicos antes dos genéricos. finally serve para limpeza no fluxo normal de saída do bloco; um return ali pode substituir o resultado pendente. Não o apresentes como garantia contra interrupção abrupta do processo ou falha da máquina.
Laboratório de pedidos
O exemplo abaixo aceita uma entrada limitada em tamanho pelo chamador e valida um contrato pequeno. Primeiro prevê o resultado do pedido válido: batchId continua 00042 e attempts continua zero. Depois substitui o corpo por [], null, um objeto sem attempts, attempts como string e um inteiro fora do intervalo. Nenhuma dessas entradas deve ser convertida automaticamente para uma tarefa válida. Executa cada alteração de forma isolada e compara a classe de falha. O exemplo não implementa autenticação, autorização ou persistência; esses controlos ficam no fluxo da aplicação. Para um endpoint real, documenta também a política de campos desconhecidos e o limite do corpo antes da descodificação.
Resumo e aplicação ao suporte
Quando uma integração começa a rejeitar pedidos depois de uma mudança, recolhe uma amostra sintética que reproduza o formato sem incluir dados de clientes. Compara a estrutura anterior com a atual: tipo da raiz, identidade textual, campos, tipos e intervalo. Se o erro aparecer apenas na resposta, verifica também valores não representáveis em JSON, como NAN, e texto com codificação inválida. Não resolvas uma falha de contrato adicionando casts até o teste passar. Guarda o caso como teste de regressão com o resultado esperado. Liga esta aula às comparações estritas, à autorização por recurso e às transações: passar a fronteira JSON torna os dados interpretáveis, mas não autoriza nem confirma a operação.
<?php
declare(strict_types=1);
function parseTask(string $body): array {
$value = json_decode($body, false, 32, JSON_THROW_ON_ERROR);
if (!$value instanceof stdClass) {
throw new InvalidArgumentException('Expected an object');
}
if (!property_exists($value, 'batchId') || !is_string($value->batchId)
|| $value->batchId === '') {
throw new InvalidArgumentException('Expected a textual batchId');
}
if (!property_exists($value, 'attempts') || !is_int($value->attempts)
|| $value->attempts < 0 || $value->attempts > 5) {
throw new InvalidArgumentException('Expected attempts from 0 to 5');
}
return ['batchId' => $value->batchId, 'attempts' => $value->attempts];
}
echo json_encode(parseTask('{"batchId":"00042","attempts":0}'), JSON_THROW_ON_ERROR);
Uma integração troca um objeto vazio por uma lista vazia. A sintaxe continua válida, mas o parser rejeita a raiz antes de tentar criar a tarefa.
Armadilhas comuns
Não tratar null válido como erro de sintaxe, converter IDs para números, ignorar falhas de serialização ou devolver sucesso depois de um TypeError.
Tópicos relacionados: Tipos e comparações explícitas · Arrays e funções com intenção · Transações e falhas parciais em PDO
Descodificar não valida o negócio: conserva identidade, declara a estrutura e liga cada erro à operação que o produziu.
Referência: PHP manual: json_decode · PHP 8.5 reference; DR PHP 2026.3; new fixtures executed on PHP 8.4.4 / SQLite 3.51.2