Dejme tomu, že máme složitý objekt a rádi bychom jej konvertovali na řetězec, abychom jej poslali po síti nebo ho jen vypsali pro účely logování.
Samozřejmě by takový řetězec měl obsahovat všechny důležité vlastnosti.
Můžeme implementovat konverzi například takto:
let uživatel = {
jméno: "Jan",
věk: 30,
toString() {
return `{jméno: "${this.jméno}", věk: ${this.věk}}`;
}
};
alert(uživatel); // {jméno: "Jan", věk: 30}
…Během procesu vývoje se však přidávají nové vlastnosti, staré se přejmenovávají a odstraňují. Pokaždé měnit takový toString by mohlo být nepříjemné. Mohli bychom se pokusit vytvořit cyklus nad vlastnostmi, ale co když je objekt složitý a obsahuje ve vlastnostech vnořené objekty? Museli bychom implementovat i jejich konverzi.
Naštěstí kód, který by tohle všechno zvládl, není třeba psát. Tento úkol byl již vyřešen.
JSON.stringify
JSON (JavaScript Object Notation – JavaScriptová objektová notace) je obecný formát sloužící k reprezentaci hodnot a objektů. Je popsán ve standardu RFC 4627. Původně byl vytvořen pro JavaScript, ale knihovny pro práci s ním poskytuje i mnoho jiných jazyků. Je tedy snadné používat JSON pro výměnu dat, když klient používá JavaScript a server je napsán v Ruby, PHP, Javě nebo čemkoli jiném.
JavaScript poskytuje metody:
JSON.stringifypro konverzi objektů na JSON.JSON.parsepro konverzi JSONu zpět na objekty.
Například zde zavoláme JSON.stringify na studenta:
let student = {
jméno: 'Jan',
věk: 30,
jeSprávce: false,
kurzy: ['html', 'css', 'js'],
choť: null
};
let json = JSON.stringify(student);
alert(typeof json); // získali jsme řetězec!
alert(json);
/* objekt zakódovaný do JSON:
{
"jméno": "Jan",
"věk": 30,
"jeSprávce": false,
"kurzy": ["html", "css", "js"],
"choť": null
}
*/
Metoda JSON.stringify(student) vezme objekt a konvertuje ho na řetězec.
Výsledný řetězec json se nazývá JSONem zakódovaný, serializovaný, zřetězený nebo marshallovaný objekt. Jsme připraveni poslat ho po drátě nebo umístit ho do úložiště planých dat.
Prosíme všimněte si, že objekt zakódovaný JSONem má několik důležitých rozdílů oproti objektovému literálu:
- Řetězce jsou uzavřeny v dvojitých uvozovkách. JSON nezná jednoduché ani zpětné uvozovky. Z
'Jan'se tedy stane"Jan". - Názvy vlastností objektů jsou rovněž v dvojitých uvozovkách. To je povinné. Z
věk:30se tedy stane"věk":30.
JSON.stringify lze aplikovat i na primitivy.
JSON podporuje následující datové typy:
- Objekty
{ ... } - Pole
[ ... ] - Primitivy:
- řetězce,
- čísla,
- booleovské hodnoty
true/false, null.
Příklad:
// číslo v JSON je prostě číslo
alert( JSON.stringify(1) ) // 1
// řetězec v JSON je stále řetězec, ale v uvozovkách
alert( JSON.stringify('test') ) // "test"
alert( JSON.stringify(true) ); // true
alert( JSON.stringify([1, 2, 3]) ); // [1,2,3]
JSON je jazykově nezávislá specifikace určená jen pro data, takže některé objektové vlastnosti specifické pro JavaScript JSON.stringify vynechává.
Konkrétně:
- Funkční vlastnosti (metody).
- Symbolické klíče a hodnoty.
- Vlastnosti, v nichž je uloženo
undefined.
let uživatel = {
řekniAhoj() { // ignoruje se
alert("Ahoj");
},
[Symbol("id")]: 123, // ignoruje se
něco: undefined // ignoruje se
};
alert( JSON.stringify(uživatel) ); // {} (prázdný objekt)
Obvykle je to dobře. Pokud to chceme nějak jinak, brzy uvidíme, jak si tento proces můžeme přizpůsobit.
Skvělé je, že vnořené objekty jsou podporovány a automaticky se konvertují.
Příklad:
let mítink = {
titul: "Konference",
místnost: {
číslo: 23,
účastníci: ["jan", "anna"]
}
};
alert( JSON.stringify(mítink) );
/* Celá struktura je zřetězena:
{
"titul":"Konference",
"místnost":{"číslo":23,"účastníci":["jan","anna"]},
}
*/
Důležité omezení: v objektu nesmějí být kruhové odkazy.
Příklad:
let místnost = {
číslo: 23
};
let mítink = {
titul: "Konference",
účastníci: ["jan", "anna"]
};
mítink.místo = místnost; // mítink se odkazuje na místnost
místnost.obsazenoČím = mítink; // místnost se odkazuje na mítink
JSON.stringify(mítink); // Error: Converting circular structure to JSON
Zde konverze selže kvůli kruhovému odkazu: místnost.obsazenoČím se odkazuje na mítink a mítink.místo se odkazuje na místnost:
Vyloučení a transformace: funkce nahrazení
Úplná syntaxe JSON.stringify je:
let json = JSON.stringify(hodnota[, nahrazení, mezery])
- hodnota
- Hodnota k zakódování.
- nahrazení
- Pole vlastností, které se mají zakódovat, nebo mapovací funkce
function(klíč, hodnota). - mezery
- Počet mezer, které se použijí při formátování.
Většinou se JSON.stringify používá jen s prvním argumentem. Jestliže však potřebujeme proces nahrazení vyladit, například odfiltrovat kruhové odkazy, můžeme použít druhý argument funkce JSON.stringify.
Předáme-li do něj pole vlastností, budou zakódovány pouze vlastnosti uvedené v tomto poli.
Příklad:
let místnost = {
číslo: 23
};
let mítink = {
titul: "Konference",
účastníci: [{jméno: "Jan"}, {jméno: "Alice"}],
místo: místnost // mítink se odkazuje na místnost
};
místnost.obsazenoČím = mítink; // místnost se odkazuje na mítink
alert( JSON.stringify(mítink, ['titul', 'účastníci']) );
// {"titul":"Konference","účastníci":[{},{}]}
Tady jsme asi příliš přísní. Seznam vlastností se použije na celou strukturu. Objekty v poli účastníci jsou tedy prázdné, protože jméno není na seznamu.
Zahrňme na seznam všechny vlastnosti kromě místnost.obsazenoČím, která by způsobila kruhový odkaz:
let místnost = {
číslo: 23
};
let mítink = {
titul: "Konference",
účastníci: [{jméno: "Jan"}, {jméno: "Alice"}],
místo: místnost // mítink se odkazuje na místnost
};
místnost.obsazenoČím = mítink; // místnost se odkazuje na mítink
alert( JSON.stringify(mítink, ['titul', 'účastníci', 'místo', 'jméno', 'číslo']) );
/*
{
"titul":"Konference",
"účastníci":[{"jméno":"Jan"},{"jméno":"Alice"}],
"místo":{"číslo":23}
}
*/
Nyní je serializováno všechno kromě obsazenoČím. Seznam vlastností je však poměrně dlouhý.
Naštěstí můžeme jako nahrazení místo pole použít funkci.
Tato funkce bude volána pro každou dvojici (klíč, hodnota) a měla by vracet „nahrazenou“ hodnotu, která bude použita místo původní, nebo undefined, jestliže hodnota má být přeskočena.
V našem případě můžeme vracet hodnotu hodnota tak, „jak je“, pro všechno kromě obsazenoČím. Abychom obsazenoČím ignorovali, následující kód vrátí undefined:
let místnost = {
číslo: 23
};
let mítink = {
titul: "Konference",
účastníci: [{jméno: "Jan"}, {jméno: "Alice"}],
místo: místnost // mítink se odkazuje na místnost
};
místnost.obsazenoČím = mítink; // místnost se odkazuje na mítink
alert( JSON.stringify(mítink, function nahrazení(klíč, hodnota) {
alert(`${klíč}: ${hodnota}`);
return (klíč == 'obsazenoČím') ? undefined : hodnota;
}));
/* dvojice klíč:hodnota, které vstoupily do nahrazení:
: [object Object]
titul: Konference
účastníci: [object Object],[object Object]
0: [object Object]
jméno: Jan
1: [object Object]
jméno: Alice
místo: [object Object]
číslo: 23
obsazenoČím: [object Object]
*/
Prosíme všimněte si, že funkce nahrazení obdrží každou dvojici klíč/hodnota včetně vnořených objektů a prvků pole. Aplikuje se rekurzívně. Hodnota this uvnitř funkce nahrazení je objekt, který obsahuje aktuální vlastnost.
První volání je speciální. Učiní se pomocí speciálního „obalového objektu“: {"": mítink}. Jinými slovy, první dvojice (klíč, hodnota) má prázdný klíč a hodnotou je celý cílový objekt. Z tohoto důvodu je první řádek ve výše uvedeném příkladu ":[object Object]".
Stojí za tím úmysl poskytnout funkci nahrazení co největší moc: má možnost analyzovat a nahradit či přeskočit i celý objekt, je-li to nutné.
Formátování: mezery
Třetí argument JSON.stringify(hodnota, nahrazení, mezery) je počet mezer, které se použijí pro pěkné formátování.
Žádné objekty převedené na řetězce, které jsme doposud uvedli, neměly odsazení a mezery navíc. To je dobře, jestliže chceme poslat objekt po síti. Argument mezery se používá výhradně pro zkrášlení výstupu.
Zde mezery = 2 říká JavaScriptu, aby zobrazil vnořené objekty na více řádcích s odsazením 2 mezery uvnitř objektu:
let uživatel = {
jméno: "Jan",
věk: 25,
role: {
jeSprávce: false,
jeEditor: true
}
};
alert(JSON.stringify(uživatel, null, 2));
/* dvoumezerová odsazení:
{
"jméno": "Jan",
"věk": 25,
"role": {
"jeSprávce": false,
"jeEditor": true
}
}
*/
/* pro JSON.stringify(uživatel, null, 4) by výsledek byl více odsazený:
{
"jméno": "Jan",
"věk": 25,
"role": {
"jeSprávce": false,
"jeEditor": true
}
}
*/
Třetí argument může být i řetězec. V tom případě se pro odsazení místo stanoveného počtu mezer použije tento řetězec.
Parametr mezery se používá výhradně pro účely logování a pěkného výstupu.
Vlastní „toJSON“
Stejně jako toString pro konverzi na řetězec může objekt poskytovat i metodu toJSON pro konverzi na JSON. Pokud je k dispozici, JSON.stringify ji automaticky zavolá.
Příklad:
let místnost = {
číslo: 23
};
let mítink = {
titul: "Konference",
datum: new Date(Date.UTC(2017, 0, 1)),
místnost
};
alert( JSON.stringify(mítink) );
/*
{
"titul":"Konference",
"datum":"2017-01-01T00:00:00.000Z", // (1)
"místnost": {"číslo":23} // (2)
}
*/
Zde vidíme, že datum (1) se stalo řetězcem. Je to tím, že všechna data mají vestavěnou metodu toJSON, která vrátí řetězec tohoto druhu.
Nyní přidáme vlastní metodu toJSON do našeho objektu místnost (2):
let místnost = {
číslo: 23,
toJSON() {
return this.číslo;
}
};
let mítink = {
titul: "Konference",
místnost
};
alert( JSON.stringify(místnost) ); // 23
alert( JSON.stringify(mítink) );
/*
{
"titul":"Konference",
"místnost": 23
}
*/
Jak vidíme, toJSON se používá jak při přímém volání JSON.stringify(místnost), tak tehdy, když je místnost vnořená v jiném kódovaném objektu.
JSON.parse
K dekódování řetězce v JSONu potřebujeme další metodu, která se nazývá JSON.parse.
Syntaxe:
let hodnota = JSON.parse(řetězec[, oživení]);
- řetězec
- Řetězec v JSONu, který se má dekódovat.
- oživení
- Nepovinná funkce
function(klíč, hodnota), která bude volána pro každou dvojici(klíč, hodnota)a může hodnotu transformovat.
Příklad:
// zřetězené pole
let čísla = "[0, 1, 2, 3]";
čísla = JSON.parse(čísla);
alert( čísla[1] ); // 1
Nebo pro vnořené objekty:
let dataUživatele = '{ "jméno": "Jan", "věk": 35, "jeSprávce": false, "přátelé": [0,1,2,3] }';
let uživatel = JSON.parse(dataUživatele);
alert( uživatel.přátelé[1] ); // 1
JSON může být tak složitý, jak potřebujeme, objekty a pole mohou obsahovat jiné objekty a pole. Všechny však musejí dodržovat stejný formát JSON.
Toto jsou typické chyby v ručně psaném JSONu (někdy jej musíme napsat ručně pro účely ladění):
let json = `{
jméno: "Jan", // chyba: název vlastnosti bez uvozovek
"příjmení": 'Novák', // chyba: hodnota v jednoduchých uvozovkách (musí být ve dvojitých)
'jeSprávce': false // chyba: klíč v jednoduchých uvozovkách (musí být ve dvojitých)
"datumNarození": new Date(2000, 2, 3), // chyba: není povoleno "new", jen čisté hodnoty
"přátelé": [0,1,2,3] // zde je vše v pořádku
}`;
Kromě toho JSON nepodporuje komentáře. Přidání komentáře do JSONu jej učiní neplatným.
Existuje i jiný formát jménem JSON5, který umožňuje klíče bez uvozovek, komentáře a podobně. To je však samostatná knihovna, která není obsažena ve specifikaci jazyka.
Standardní JSON je tak přísný ne proto, že by jeho vývojáři byli líní, ale proto, aby umožnil snadnou, spolehlivou a velmi rychlou implementaci načítacího algoritmu.
Použití oživení
Představme si, že jsme ze serveru získali zřetězený objekt mítink.
Vypadá takto:
// titul: (titul mítinku), datum: (datum mítinku)
let řetězec = '{"titul":"Konference","datum":"2017-11-30T12:00:00.000Z"}';
…A nyní jej musíme deserializovat, aby se z něj znovu stal JavaScriptový objekt.
Učiníme to voláním JSON.parse:
let řetězec = '{"titul":"Konference","datum":"2017-11-30T12:00:00.000Z"}';
let mítink = JSON.parse(řetězec);
alert( mítink.datum.getDate() ); // Chyba!
Ouha! Chyba!
Hodnota mítink.datum je řetězec, ne objekt Date. Jak mohl JSON.parse vědět, že má tento řetězec transformovat do objektu Date?
Jako druhý argument předáme do JSON.parse funkci oživení, která vrátí všechny hodnoty tak, „jak jsou“, ale datum změní na Date:
let řetězec = '{"titul":"Konference","datum":"2017-11-30T12:00:00.000Z"}';
let mítink = JSON.parse(řetězec, function(klíč, hodnota) {
if (klíč == 'datum') return new Date(hodnota);
return hodnota;
});
alert( mítink.datum.getDate() ); // teď to funguje!
Mimochodem, funguje to stejně i pro vnořené objekty:
let rozvrh = `{
"seznamMítinků": [
{"titul":"Konference","datum":"2017-11-30T12:00:00.000Z"},
{"titul":"Narozeniny","datum":"2017-04-18T12:00:00.000Z"}
]
}`;
rozvrh = JSON.parse(rozvrh, function(klíč, hodnota) {
if (klíč == 'datum') return new Date(hodnota);
return hodnota;
});
alert( rozvrh.seznamMítinků[1].datum.getDate() ); // funguje to!
Shrnutí
- JSON je datový formát, který má svůj nezávislý standard a knihovny pro většinu programovacích jazyků.
- JSON podporuje plané objekty, pole, řetězce, čísla, booleany a
null. - JavaScript poskytuje metody JSON.stringify pro serializaci do JSONu a JSON.parse pro načítání z JSONu.
- Obě metody podporují transformační funkce pro vylepšené načítání a zápis.
- Má-li objekt metodu
toJSON, pak ji funkceJSON.stringifyzavolá.
Komentáře
<code>, pro několik řádků je obalte značkou<pre>, pro více než 10 řádků vložte odkaz na pískoviště (plnkr, jsbin, codepen…)