Ipotizziamo di avere un oggetto complesso, che vogliamo convertire a stringa prima trasmetterlo in rete, o anche solo per mostrarlo sullo schermo.
Naturalmente, una stringa di questo tipo deve includere tutte le proprietà importanti.
Potremmo implementarla in questo modo:
let user = {
name: "John",
age: 30,
toString() {
return `{name: "${this.name}", age: ${this.age}}`;
}
};
alert(user); // {name: "John", age: 30}
…Ma nel processo di sviluppo, potrebbero essere aggiunte/eliminate/rinominate nuove proprietà. Aggiornare costantemente la funzione toString
non è una buona soluzione. Potremmo provare a iterare sulle proprietà dell’oggetto, ma cosa succederebbe se l’oggetto contenesse oggetti annidati? Dovremmo implementare anche questa conversione. E se dovessimo inviare l’oggetto in rete, dovremmo anche fornire il codice per poterlo “leggere”.
Fortunatamente, non c’è bisogno di scrivere del codice per gestire questa situazione.
JSON.stringify
JSON (JavaScript Object Notation) è un formato per rappresentare valori e oggetti. Viene descritto nello standard RFC 4627. Inizialmente fu creato per lavorare con JavaScript, ma molti altri linguaggi possiedono delle librerie per la sua gestione. Quindi JSON risulta semplice da usare per lo scambio di dati quando il client utilizza JavaScript e il server codifica in Ruby/PHP/Java/Altro.
JavaScript fornisce i metodi:
JSON.stringify
per convertire oggetti in JSON.JSON.parse
per convertire JSON in oggetto.
Ad esempio, abbiamo JSON.stringify
per uno studente:
let student = {
name: 'John',
age: 30,
isAdmin: false,
courses: ['html', 'css', 'js'],
wife: null
};
let json = JSON.stringify(student);
alert(typeof json); //abbiamo una stringa!
alert(json);
/* JSON-encoded object:
{
"name": "John",
"age": 30,
"isAdmin": false,
"courses": ["html", "css", "js"],
"wife": null
}
*/
Il metodo JSON.stringify(student)
prende l’oggetto e lo converte in stringa.
La stringa json
risultante viene chiamata codifica in JSON o serializzata, stringhificata, o addirittura oggetto caramellizzato. Ora è pronto per essere inviato o memorizzato.
Da notare che un oggetto codificato in JSON possiede delle fondamentali differenze da un oggetto letterale:
- Le stringhe utilizzano doppie virgolette. In JSON non vengono utilizzare backtick o singole virgolette. Quindi
'John'
diventa"John"
. - Anche i nomi delle proprietà dell’oggetto vengono racchiusi tra doppie virgolette. Quindi
age:30
diventa"age":30
.
JSON.stringify
può anche essere applicato agli oggetti primitivi.
I tipi che supportano nativamente JSON sono:
- Objects
{ ... }
- Arrays
[ ... ]
- Primitives:
- strings,
- numbers,
- valori boolean
true/false
, null
.
Ad esempio:
// un numero in JSON è solo un numero
alert( JSON.stringify(1) ) // 1
// una stringa in JSON è ancora una stringa, ma con doppie virgolette
alert( JSON.stringify('test') ) // "test"
alert( JSON.stringify(true) ); // true
alert( JSON.stringify([1, 2, 3]) ); // [1,2,3]
JSON è un linguaggio specifico per i dati, quindi alcune proprietà specifiche di JavaScript vengono ignorate da JSON.stringify
.
Tra cui:
- Funzioni (metodi).
- Proprietà di tipo symbol.
- Proprietà che contengono
undefined
.
let user = {
sayHi() { // ignorato
alert("Hello");
},
[Symbol("id")]: 123, // ignorato
something: undefined // ignorato
};
alert( JSON.stringify(user) ); // {} (oggetto vuoto)
Solitamente questo è ciò che vogliamo. Se invece abbiamo intenzioni diverse, molto presto vedremo come controllare il processo.
Un’ottima cosa è che anche gli oggetti annidati vengono automaticamente convertiti.
Ad esempio:
let meetup = {
title: "Conference",
room: {
number: 23,
participants: ["john", "ann"]
}
};
alert( JSON.stringify(meetup) );
/* Tutta la struttura viene serializzata
{
"title":"Conference",
"room":{"number":23,"participants":["john","ann"]},
}
*/
Una limitazione importante: non ci devono essere riferimenti ciclici.
Ad esempio:
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: ["john", "ann"]
};
meetup.place = room; // meetup referenzia room
room.occupiedBy = meetup; // room referenzia meetup
JSON.stringify(meetup); // Errore: conversione di struttura circolare in JSON
In questo caso, la conversione fallisce a causa del riferimento ciclico: room.occupiedBy
fa riferimento a meetup
, e meetup.place
fa riferimento a room
:
Esclusione e rimpiazzo: replacer
La sintassi completa di JSON.stringify
è:
let json = JSON.stringify(value[, replacer, space])
- value
- Valore da codificare.
- replacer
- Array di proprietà da codificare o una funzione di mapping
function(key, value)
. - space
- Quantità di spazio da utilizzare per la formattazione
Nella maggior parte dei casi, JSON.stringify
viene utilizzato specificando solamente il primo argomento. Ma se avessimo bisogno di gestire il processo di rimpiazzo, ad esempio filtrando i riferimenti ciclici, possiamo utilizzare il secondo argomento di JSON.stringify
.
Se forniamo un array di proprietà, solamente quelle proprietà verranno codificate.
Ad esempio:
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup referenzia room
};
room.occupiedBy = meetup; // room referenzia meetup
alert( JSON.stringify(meetup, ['title', 'participants']) );
// {"title":"Conference","participants":[{},{}]}
Qui, probabilmente, siamo stati troppo rigidi. La lista di proprietà viene applicata all’intera struttura dell’oggetto. Quindi participants
risulta essere vuoto, perché name
non è in lista.
Andiamo ad includere ogni proprietà ad eccezione di room.occupiedBy
, che potrebbe causare un riferimento ciclico:
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup referenzia room
};
room.occupiedBy = meetup; // room referenzia meetup
alert( JSON.stringify(meetup, ['title', 'participants', 'place', 'name', 'number']) );
/*
{
"title":"Conference",
"participants":[{"name":"John"},{"name":"Alice"}],
"place":{"number":23}
}
*/
Ora tutto, ad eccezione di occupiedBy
, viene serializzato. Ma la lista di proprietà è piuttosto lunga.
Fortunatamente, possiamo utilizzare come replacer
una funzione piuttosto di un array.
La funzione verrà invocata per ogni coppia (key, value)
e dovrebbe ritornare il valore sostitutivo, che verrà utilizzato al posto di quello originale.
Nel nostro caso, possiamo ritornare value
(il valore stesso della proprietà) in tutti i casi, ad eccezione di occupiedBy
. Per poter ignorare occupiedBy
, il codice sotto ritorna undefined
:
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup referenzia room
};
room.occupiedBy = meetup; // room referenzia meetup
alert( JSON.stringify(meetup, function replacer(key, value) {
alert(`${key}: ${value}`);
return (key == 'occupiedBy') ? undefined : value;
}));
/* coppie key:value passate al replacer:
: [object Object]
title: Conference
participants: [object Object],[object Object]
0: [object Object]
name: John
1: [object Object]
name: Alice
place: [object Object]
number: 23
occupiedBy: [object Object]
*/
Da notare che la funzione replacer
ottiene tutte le coppie key/value, incluse quelle degli oggetti annidati e viene applicata ricorsivamente. Il valore di this
all’interno di replacer
è l’oggetto che contiene la proprietà corrente.
La prima chiamata è speciale. Viene effettuata utilizzando uno speciale “oggetto contenitore”: {"": meetup}
. In altre parole, la prima coppia (key, value)
possiede una chiave vuota, e il valore è l’oggetto stesso. Questo è il motivo per cui la prima riga dell’esempio sopra risulta essere ":[object Object]"
.
L’idea è quella di fornire più potenza possibile a replacer
: deve avere la possibilità di rimpiazzare/saltare l’oggetto stesso, se necessario.
Formattazione: spacer
Il terzo argomento di JSON.stringify(value, replacer, spaces)
è il numero di spazi da utilizzare per una corretta formattazione.
Tutti gli oggetti serializzati fino ad ora non possedevano una indentazione o spazi extra. Ci può andare bene se dobbiamo semplicemente inviare l’oggetto. L’argomento spacer
viene utilizzato solo con lo scopo di abbellirlo.
In questo caso spacer = 2
dice a JavaScript di mostrare gli oggetti annidati in diverse righe, con un indentazione di 2 spazi all’interno dell’oggetto:
let user = {
name: "John",
age: 25,
roles: {
isAdmin: false,
isEditor: true
}
};
alert(JSON.stringify(user, null, 2));
/* indentazione di due spazi:
{
"name": "John",
"age": 25,
"roles": {
"isAdmin": false,
"isEditor": true
}
}
*/
/* Per JSON.stringify(user, null, 4) il risultato sarebbe più indentato:
{
"name": "John",
"age": 25,
"roles": {
"isAdmin": false,
"isEditor": true
}
}
*/
Il parametro spaces
viene utilizzato unicamente per procedure di logging o per abbellire l’output.
Modificare “toJSON”
Come per la conversione toString
, un oggetto può fornire un metodo toJSON
per la conversione a JSON. Se questa è disponibile JSON.stringify
la chiamerà automaticamente.
Ad esempio:
let room = {
number: 23
};
let meetup = {
title: "Conference",
date: new Date(Date.UTC(2017, 0, 1)),
room
};
alert( JSON.stringify(meetup) );
/*
{
"title":"Conference",
"date":"2017-01-01T00:00:00.000Z", // (1)
"room": {"number":23} // (2)
}
*/
Qui possiamo vedere che date
(1)
diventa una stringa. Questo accade perché tutti gi oggetti di tipo Date
possiedono un metodo toJSON
.
Ora proviamo ad aggiungere un metodo toJSON
personalizzato per il nostro oggetto room
(2)
:
let room = {
number: 23,
toJSON() {
return this.number;
}
};
let meetup = {
title: "Conference",
room
};
alert( JSON.stringify(room) ); // 23
alert( JSON.stringify(meetup) );
/*
{
"title":"Conference",
"room": 23
}
*/
Come possiamo vedere, toJSON
viene utilizzato sia per le chiamate dirette a JSON.stringify(room)
, sia per gli oggetti annidati.
JSON.parse
Per decodificare una stringa in JSON, abbiamo bisogno del metodo JSON.parse.
La sintassi:
let value = JSON.parse(str, [reviver]);
- str
- stringa JSON da decodificare.
- reviver
- funzione(key, value) che verrà chiamata per ogni coppia
(key, value)
e che può rimpiazzare i valori.
Ad esempio:
// array serializzato
let numbers = "[0, 1, 2, 3]";
numbers = JSON.parse(numbers);
alert( numbers[1] ); // 1
Nel caso di oggetti annidati:
let userData = '{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }';
let user = JSON.parse(userData);
alert( user.friends[1] ); // 1
Il JSON può essere complesso quanto vogliamo, gli oggetti e array possono includere altri oggetti e array. Ma devono seguire il corretto formato.
Alcuni errori tipici di scrittura in JSON (in qualche caso lo utilizzeremo per scopi di debug):
let json = `{
name: "John", // errore: proprietà name without quotes
"surname": 'Smith', // errore: singole virgolette in value (devono essere doppie)
'isAdmin': false // errore: singole virgolette in key (devono essere doppie)
"birthday": new Date(2000, 2, 3), // errore: "new" non è permesso
"friends": [0,1,2,3] // qui tutto bene
}`;
Inoltre, JSON non supporta i commenti. Quindi l’aggiunta di un commento invaliderebbe il documento.
Esiste un altro formato chiamato JSON5, che accetta chiavi senza virgolette, commenti etc. Ma consiste in una libreria a se stante, non fa parte della specifica del linguaggio.
Il JSON classico è cosi restrittivo non per pigrizia degli sviluppatori, ma per consentire una facile, affidabile e rapida implementazione degli algoritmi di analisi.
Ritrasformare in oggetto
Immaginate di ricevere dal server un oggetto meetup
serializzato.
Come:
// title: (meetup title), date: (meetup date)
let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';
…Ora abbiamo bisogno di deserializzarlo, per ricreare l’oggetto.
Lo facciamo chiamando JSON.parse
:
let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';
let meetup = JSON.parse(str);
alert( meetup.date.getDate() ); // Error!
Whoops! Errore!
Il valore di meetup.date
è una stringa, non un oggetto di tipo Date
. Come fa JSON.parse
a sapere che dovrebbe trasformare quella stringa in un oggetto di tipo Date
?
Passiamo a JSON.parse
la funzione di riattivazione che ritorna tutti i valori per “come sono”, quindi date
diventerà Date
:
let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';
let meetup = JSON.parse(str, function(key, value) {
if (key == 'date') return new Date(value);
return value;
});
alert( meetup.date.getDate() ); // now works!
Funziona anche per gli oggetti annidati:
let schedule = `{
"meetups": [
{"title":"Conference","date":"2017-11-30T12:00:00.000Z"},
{"title":"Birthday","date":"2017-04-18T12:00:00.000Z"}
]
}`;
schedule = JSON.parse(schedule, function(key, value) {
if (key == 'date') return new Date(value);
return value;
});
alert( schedule.meetups[1].date.getDate() ); // works!
Riepilogo
- JSON è un formattatore di dati con i suoi standard; possiede molte librerie che gli consentono di lavorare con altrettanti linguaggi di programmazione.
- JSON supporta oggetti, array, stringhe, numeri, booleani, e
null
. - JavaScript fornisce dei metodi: JSON.stringify per serializzare in JSON e JSON.parse per la lettura da JSON.
- Entrambi i metodi supportano funzioni di rimpiazzo per scritture/letture “intelligenti”.
- Se un oggetto implementa
toJSON
, questa verrà automaticamente invocata daJSON.stringify
.