Introduzione
Quando un’applicazione deve modificare una risorsa descritta in formato JSON, non è sempre necessario inviare nuovamente l’intero documento. Se un profilo contiene nome, email, indirizzo e preferenze, per cambiare soltanto l’email sarebbe inefficiente trasmettere tutti gli altri campi.
Una soluzione pratica è utilizzare il JSON Merge Patch, definito dallo standard RFC 7396. Un merge patch rappresenta le modifiche da applicare a un documento JSON esistente:
- le proprietà presenti nella patch vengono aggiunte o aggiornate;
- le proprietà con valore
nullvengono eliminate; - le proprietà non presenti nella patch rimangono invariate;
- un oggetto annidato viene modificato senza dover riscrivere l’intera struttura.
Questo approccio è particolarmente utile per form di modifica, preferenze utente, documenti amministrativi e aggiornamenti parziali ricevuti da un client. In questo tutorial costruiremo una piccola implementazione in PHP, con validazione dei dati e gestione delle proprietà eliminabili.
Codice completo
<?php
declare(strict_types=1);
/**
* Applica una JSON Merge Patch a un documento PHP.
*
* Regole principali:
* - un oggetto nella patch aggiorna ricorsivamente il documento;
* - null elimina una proprietà;
* - un valore scalare o un array sostituisce il valore esistente.
*/
function applyMergePatch(mixed $document, mixed $patch): mixed
{
// Se la patch non è un oggetto JSON, sostituisce completamente il documento.
if (!is_array($patch) || isList($patch)) {
return $patch;
}
// Per modificare proprietà, il documento deve essere un oggetto.
if (!is_array($document) || isList($document)) {
$document = [];
}
foreach ($patch as $property => $value) {
if ($value === null) {
// In JSON Merge Patch, null significa "rimuovi la proprietà".
unset($document[$property]);
continue;
}
if (is_array($value) && !isList($value)) {
// Aggiornamento ricorsivo di un oggetto annidato.
$existingValue = $document[$property] ?? [];
$document[$property] = applyMergePatch($existingValue, $value);
} else {
// Stringhe, numeri, booleani e array sostituiscono il valore.
$document[$property] = $value;
}
}
return $document;
}
/**
* Determina se un array PHP rappresenta una lista JSON.
*/
function isList(array $value): bool
{
return array_is_list($value);
}
/**
* Decodifica un JSON e verifica che sia valido.
*/
function decodeJson(string $json, string $description): mixed
{
try {
return json_decode(
$json,
true,
512,
JSON_THROW_ON_ERROR
);
} catch (JsonException $exception) {
throw new InvalidArgumentException(
"JSON non valido per {$description}: " . $exception->getMessage(),
0,
$exception
);
}
}
/**
* Codifica un valore PHP in JSON leggibile.
*/
function encodeJson(mixed $value): string
{
return json_encode(
$value,
JSON_PRETTY_PRINT |
JSON_UNESCAPED_UNICODE |
JSON_THROW_ON_ERROR
);
}
$documentJson = <<<´JSON´
{
"id": 42,
"nome": "Elena Rossi",
"email": "[email protected]",
"attiva": true,
"indirizzo": {
"citta": "Milano",
"cap": "20100",
"via": "Via Roma 10"
},
"preferenze": {
"tema": "chiaro",
"newsletter": true
},
"tag": ["cliente", "premium"]
}
JSON;
$patchJson = <<<´JSON´
{
"email": "[email protected]",
"indirizzo": {
"cap": "20121",
"via": null
},
"preferenze": {
"tema": "scuro"
},
"tag": ["cliente", "premium", "mobile"]
}
JSON;
try {
$document = decodeJson($documentJson, ´documento originale´);
$patch = decodeJson($patchJson, ´merge patch´);
$updatedDocument = applyMergePatch($document, $patch);
echo encodeJson($updatedDocument);
} catch (InvalidArgumentException | JsonException $exception) {
http_response_code(400);
echo ´Errore: ´ . $exception->getMessage();
} Spiegazione
Distinguere oggetti e liste
In PHP, sia gli oggetti JSON sia gli array JSON vengono generalmente decodificati in array quando si usa il secondo parametro di json_decode() impostato a true. È quindi necessario distinguere un oggetto da una lista.
La funzione array_is_list(), disponibile da PHP 8.1, restituisce true quando le chiavi sono numeriche consecutive, ad esempio 0, 1, 2. Un array associativo, invece, viene trattato come un oggetto JSON.
Questa distinzione è importante perché gli oggetti vengono uniti ricorsivamente, mentre gli array vengono sostituiti completamente. Per esempio, se il documento contiene:
{
"tag": ["php", "json", "web"]
} e la patch contiene:
{
"tag": ["php", "api"]
} il risultato avrà soltanto ["php", "api"]. JSON Merge Patch non aggiunge o rimuove singoli elementi da una lista: sostituisce l’intero array.
Il significato di null
Nel JSON Merge Patch, null non significa necessariamente “imposta il valore a null”. Significa invece “rimuovi questa proprietà”. Nel nostro esempio, "via": null elimina la chiave via dall’oggetto indirizzo.
Questa caratteristica deve essere conosciuta dal client e dal server. Se l’applicazione deve distinguere tra “proprietà assente”, “proprietà da eliminare” e “proprietà con valore null”, il JSON Merge Patch potrebbe non essere sufficiente. In quel caso è possibile valutare JSON Patch, basato su operazioni esplicite come add, remove e replace.
Gestione degli errori
L’opzione JSON_THROW_ON_ERROR fa sollevare un’eccezione quando il JSON non può essere decodificato o codificato. È preferibile rispetto al controllo manuale di json_last_error(), perché rende il flusso degli errori più chiaro e impedisce di utilizzare dati parzialmente interpretati.
Best practice
- Valida la patch prima dell’applicazione: verifica che i campi modificabili abbiano il tipo corretto. Un campo
emaildovrebbe essere una stringa, mentreattivadovrebbe essere booleano. - Limita le proprietà consentite: non permettere al client di modificare identificativi, ruoli, timestamp interni o altri campi sensibili.
- Conserva il documento originale: la funzione dell’esempio restituisce un nuovo risultato senza modificare direttamente il documento iniziale.
- Usa transazioni quando salvi su database: applica la patch, valida il risultato finale e salva tutto in un’unica transazione.
- Gestisci la concorrenza: associa la risorsa a una versione o a un identificatore ETag, così da evitare che una patch sovrascriva modifiche più recenti.
- Imposta limiti di dimensione: JSON molto grandi o profondamente annidati possono consumare molta memoria e CPU.
- Non fidarti dei dati ricevuti: il merge patch è un meccanismo di aggiornamento, non un sistema di autorizzazione.
- Documenta il significato di null: chi sviluppa il client deve sapere che inviare
nullrimuove la proprietà.
Riepilogo
JSON Merge Patch permette di descrivere aggiornamenti parziali in modo semplice e leggibile. In PHP è possibile implementarlo combinando json_decode(), json_encode(), la distinzione tra oggetti e liste e una funzione ricorsiva.
Gli oggetti vengono aggiornati proprietà per proprietà, i valori null rimuovono le chiavi e gli array vengono sostituiti completamente. Prima di usare questa tecnica in produzione è fondamentale aggiungere autorizzazione, validazione dei campi, controllo della concorrenza e gestione sicura degli errori.
