JSON, az alapokon túl
Úgy gondolom, a JSON-t nem szükséges magyarázni, annyira elterjedt, és olyan jó leírások vannak róla. Jelen bejegyzésben elsősorban a kiterjesztési lehetőségekkel szeretnék foglalkozni.
Kyle Simpson egy tartalmas bejegyzésben kifejtette, miért hiányos szerinte a JSON specifikáció: ma már a JSON-t nem csupán adatok továbbítására, tárolására használjuk. Az előnye, hogy JavaScripttel nagyon egyszerű értelmezni, a hátrányára is vált: olyan dolgokra is használják, amire nem alkalmas, ilyen például a config fájl (jslint, jshint), vagy a template rendszerek. Mindkét esetben szeretnénk megjegyzéseket is elhelyezni, erre a JSON nem ad lehetőséget, pedig Douglas Crockford is megmondta még 2005-ben:
A Kyle által javasolt JSON+Comments nem más, mint valid JSON kiegészítve valid megjegyzésekkel. Mára a JSON annyira elterjedt, hogy a szabványon módosítani gyakorlatilag lehetetlen, ő is inkább egy
Az előbb tárgyalt hiányosság, bár valós, nem okoz olyan nagy gondot, hiszen a legtöbb esetben tényleg arra használjuk a JSON-t, amire való: adatok tárolására és továbbítására. Itt azonban egy égetőbb hiányosságba ütközünk nap, mint nap: egy JSON elemi típusú elemeket (null, boolean, number, string) és ezek (indexelt vagy hash-)tömbjét tartalmazhatja. A JavaScriptben azonban számos más típust is használunk, leggyakrabban talán a Date-et.
Mit tehet ilyenkor a programozó? (A cikk hátralevő részében az objektumunknak háromféle reprezentációját különböztetem meg: lehet JavaScript objektum, ez maga az az objektum, amivel dolgozunk. Lehet JSON string értelemszerűen, és lehet JSON objektum, amit a JSON string parse-olásával kapunk.) Két lehetőség van: vagy mindenhol az eredeti JSON objektumot adjuk át, és csak a célfelhasználás helyén hozzuk létre a JavaScript objektumokat, vagy az adatok betöltésekor végigmegyünk a struktúrán, és mindent átalakítunk. Persze, jobb lenne a második, hiszen akkor az egész programban tudnánk, hogy a megfelelő adatok vannak mindenhol, de sokféle collection-ünk lehet, azaz az értékes adatokat nagyon sokféle struktúrában kaphatjuk meg, mindegyikre külön átalakítót írni nem kifizetődő.
Szerencsére a JSON szabvány biztosít erre lehetőséget a replacer és reviver függvényekkel. Azonban ha az adatokat JSON-ban el is tároljuk (azaz a változtatás nem csak a kommunikációs réteget érinti), a projekt elindulása után már körülményes lehet az átállás.
A terv a következő: kiválasztunk egy JSON típust, amivel minden egyedi objektumot elkódolhatunk. Az ajánlott a string lenne, de ez nekem nem szimpatikus: a jelenlegi működésnél a dátumnak van
A tömböt és minden saját típust tömbként kódolunk el, ahol a JSON tömb első eleme meghatározza, hogy a többi eleme milyen típust ír le. Így már bármilyen objektumot könnyedén eltárolhatuk, és nem kell minden egyes struktúrát külön bejárni.A szemfülesebbek észrevehetik, hogy kicsit csaltam: a specifikációban a
Ez a kód egyszerű, hatékony és kevés tárolási helyet foglal. Hátránya, hogy más helyen definiáljuk az objektumot, és azt, hogy hogyan történik az oda-vissza JSON alakítás, valamint hogy mindig meg kell adni a replacer és reviver függvényeket. De mindez kiküszöbölhető:Itt már teljesen dinamikusan tudunk típust hozzáadni, amit a nevével azonosítunk (így a későbbiekben egyszerűen bővíthető lesz), ha létrehozunk egy új típust, ott helyben kell megadnunk, hogy hogyan történjen a JSON-ná alakítás, és a JSON-ná való alakítás során használhatunk bármilyen JSON-kompatibilis objektumot, az előző példában a Usert egy objektumban kódoljuk két string és egy Date objektum felhasználásával. Ugyanígy a visszaalakítás során már rendelkezésre állnak a megfelelő objektumok.
Mission completed.
A front-end developer örül, hogy megoldotta a problémát, eufórikus hangulatban újságolja a világmegváltó felfedezését back-end-es társának, aki nyakon önti egy vödör hideg vízzel: a szerver oldali nyelvekhez elérhető könyvtárak többsége nem támogatja az ilyen átalakításokat. A PHP-s megoldások közül egy sem. Pedig igazán lenne benne fantázia.
A körüljárt
■ Kyle Simpson egy tartalmas bejegyzésben kifejtette, miért hiányos szerinte a JSON specifikáció: ma már a JSON-t nem csupán adatok továbbítására, tárolására használjuk. Az előnye, hogy JavaScripttel nagyon egyszerű értelmezni, a hátrányára is vált: olyan dolgokra is használják, amire nem alkalmas, ilyen például a config fájl (jslint, jshint), vagy a template rendszerek. Mindkét esetben szeretnénk megjegyzéseket is elhelyezni, erre a JSON nem ad lehetőséget, pedig Douglas Crockford is megmondta még 2005-ben:
A JSON kódolónak nem szabad megjegyzéseket tennie a stringbe, a JSON dekódolónak viszont el kell fogadnia és figyelmen kívül hagynia őket.
A Kyle által javasolt JSON+Comments nem más, mint valid JSON kiegészítve valid megjegyzésekkel. Mára a JSON annyira elterjedt, hogy a szabványon módosítani gyakorlatilag lehetetlen, ő is inkább egy
JSON.minify()
függvényt vezetne be, ami a program számára információt nem hordozó részeket (white space-ek és kommentek) eltávolítaná. Egyet tudok érteni az ötletével.Az előbb tárgyalt hiányosság, bár valós, nem okoz olyan nagy gondot, hiszen a legtöbb esetben tényleg arra használjuk a JSON-t, amire való: adatok tárolására és továbbítására. Itt azonban egy égetőbb hiányosságba ütközünk nap, mint nap: egy JSON elemi típusú elemeket (null, boolean, number, string) és ezek (indexelt vagy hash-)tömbjét tartalmazhatja. A JavaScriptben azonban számos más típust is használunk, leggyakrabban talán a Date-et.
Mit tehet ilyenkor a programozó? (A cikk hátralevő részében az objektumunknak háromféle reprezentációját különböztetem meg: lehet JavaScript objektum, ez maga az az objektum, amivel dolgozunk. Lehet JSON string értelemszerűen, és lehet JSON objektum, amit a JSON string parse-olásával kapunk.) Két lehetőség van: vagy mindenhol az eredeti JSON objektumot adjuk át, és csak a célfelhasználás helyén hozzuk létre a JavaScript objektumokat, vagy az adatok betöltésekor végigmegyünk a struktúrán, és mindent átalakítunk. Persze, jobb lenne a második, hiszen akkor az egész programban tudnánk, hogy a megfelelő adatok vannak mindenhol, de sokféle collection-ünk lehet, azaz az értékes adatokat nagyon sokféle struktúrában kaphatjuk meg, mindegyikre külön átalakítót írni nem kifizetődő.
Szerencsére a JSON szabvány biztosít erre lehetőséget a replacer és reviver függvényekkel. Azonban ha az adatokat JSON-ban el is tároljuk (azaz a változtatás nem csak a kommunikációs réteget érinti), a projekt elindulása után már körülményes lehet az átállás.
A terv a következő: kiválasztunk egy JSON típust, amivel minden egyedi objektumot elkódolhatunk. Az ajánlott a string lenne, de ez nekem nem szimpatikus: a jelenlegi működésnél a dátumnak van
toJSON()
metódusa, de parse már nem fogja tudni, hogy az a string eredetileg egy dátum volt… Én a tömböt választottam, helytakarékossági okokból.A tömböt és minden saját típust tömbként kódolunk el, ahol a JSON tömb első eleme meghatározza, hogy a többi eleme milyen típust ír le. Így már bármilyen objektumot könnyedén eltárolhatuk, és nem kell minden egyes struktúrát külön bejárni.
// Objektum JSON-ná alakítása
function replacer(key) {
var v = this[key];
switch(true) {
case v instanceof Array: return [0].concat(v);
case v instanceof Date: return [1, v.getTime()];
case v instanceof User: return [2, v.userName, v.realName];
default: return v;
}
}
// JSON visszaalakítása
function reviver(k,v) {
if (false === v instanceof Array) {
// ami nem tömb, ahhoz nem nyúltunk hozzá
return v;
}
switch(v.shift()) {
case 0: return v;
case 1: return new Date(v.shift());
case 2: return new User(v.shift(), v.shift());
}
return null;
}
function User(userName, realName) {
this.userName = userName;
this.realName = realName;
this.userId = ++User.counter;
};
User.counter = 0;
////////////////////////////////////////////////////////////////////////////////
var obj = {
a: [1, 2, 3],
date: new Date(),
user: new User('bela', 'Kovats Bela')
};
var str = JSON.stringify(obj, replacer);
var o = JSON.parse(str, reviver);
console.log(obj); // userId: 1
console.log(str);
console.log(o); // userId: 2
replacer()
megkapja a key és value értékeket, ez utóbbit én mégis a this[key]
formában értem el. Ennek az oka, hogy a replacer()
a JSON stringbe kerülés előtti utolsó lehetőségünk, hogy bármilyen változtatást eszközöljünk (cenzúrázzunk bizonyos stringeket, de nem tudom, van-e olyan, aki valaha ezt használta volna a gyakorlatban), így ha egy objektumnak (Date) van toJSON()
metódusa, az átalakított eredményt kapjuk, ami a fentebb említettek miatt számunkra nem megfelelő.Ez a kód egyszerű, hatékony és kevés tárolási helyet foglal. Hátránya, hogy más helyen definiáljuk az objektumot, és azt, hogy hogyan történik az oda-vissza JSON alakítás, valamint hogy mindig meg kell adni a replacer és reviver függvényeket. De mindez kiküszöbölhető:
var myJSON = (function() {
var result = {};
var userTypes = {};
result.stringify = function(obj) {
return JSON.stringify(obj, function(k) {
var v = this[k];
if (v instanceof Array) {
return ['Array'].concat(v);
}
if (v instanceof Date) {
return ['Date', v.getTime()];
}
for (var type in userTypes) {
if (userTypes.hasOwnProperty(type) && v instanceof userTypes[type]) {
return [type, v.toJSON()];
}
}
return v;
});
};
result.parse = function(str) {
return JSON.parse(str, function(k,v) {
if (false === v instanceof Array) {
return v;
}
var type = v.shift();
switch(type) {
case 'Array': return v;
case 'Date': return new Date(v.shift());
default: return userTypes[type].fromJSON(v.shift());
}
return null;
});
};
result.addType = function(name, constr) {
userTypes[name] = constr;
};
return result;
}());
////////////////////////////////////////////////////////////////////////////////
function User(userName, realName, bornAt) {
this.userName = userName; // string
this.realName = realName; // string
this.bornAt = bornAt; // Date
this.userId = ++User.counter;
};
User.counter = 0;
// Hogy történjen a JSON-ná alakítás
User.prototype.toJSON = function() {
return {
u: this.userName,
r: this.realName,
b: this.bornAt
};
};
// Hogyan hozzunk létre (eredetileg) JSON-ból újra objektumot
User.fromJSON = function(data) {
return new User(data.u, data.r, data.b);
};
// A saját JSON kezelőnkbe regisztráljuk a saját típust
myJSON.addType('User', User);
////////////////////////////////////////////////////////////////////////////////
var obj = {
a: [1, 2, 3],
date: new Date(),
user: new User('bela', 'Kovats Bela', new Date(1980, 3, 4))
};
var str = myJSON.stringify(obj);
var o = myJSON.parse(str);
console.log(obj);
console.log(str);
console.log(o);
Mission completed.
A front-end developer örül, hogy megoldotta a problémát, eufórikus hangulatban újságolja a világmegváltó felfedezését back-end-es társának, aki nyakon önti egy vödör hideg vízzel: a szerver oldali nyelvekhez elérhető könyvtárak többsége nem támogatja az ilyen átalakításokat. A PHP-s megoldások közül egy sem. Pedig igazán lenne benne fantázia.
A körüljárt
replacer()
függvénynek lehet még egy harmadik paramétere, amellyel szépen formázott JSON stringeket kaphatunk, ez fejlesztés közben igen hasznos lehet, ám gyakran egyszerűbb a jsonlint-hez menni segítségért.
A PHP-s megoldások közül egy
PHP-ben van/lesz JsonSerializable , ami a replacer-nek megfelel, szóval félig már jók vagyunk/leszünk. Részletek itt. A nálam levő PHP telepítésben ez még nem létezik, így nem tudom most kipróbálni, de akinél 5.3.8 van annak érdemes lenne futni vele egy kört. A hivatkozott cikk szerint a JsonSerializable az egy interfész, a doksi szerint viszont osztály, ez érdekes. Ha tényleg osztály akkor facepalm.
Ezzel nem leszünk sokkal előrébb
Amíg nincs
reviver()
, addig az adatokat csak elkódolni tudjuk, visszanyerni viszont nem...szerintem pontosan ezt írtam,
Egyébként gratulálok a cikkhez, szépen összeszedted :)
Nem kötekedés képpen… :)
De hogy konkrétabb példát mondjak, a példányosított Date objektumnak (ez a vesszőparipám) van
.toJSON()
metódusa, a Mozilla leírása alapján:Vagyis ez nagyon hasznos, amikor egy JavaScript objektumot JSON stringbe szerializálsz (van erre magyar szó?) Mondják ők, de a gyakorlat mit mutat? Mi speciel elég sok JSON-nel dolgozunk, a dátumot többféleképpen reprezentáljuk, de sohasem string-ként.
A szerializációnak épp az lenne a lényege, hogy megadok egy objektumot, ezt átalakítja küldhető formára, majd a fogadó oldalon visszaalakul objektummá, és a fogadó már dolgozhat is vele. Ehhez képest a JSON esetén küldő oldalon először JSON objektummá kell alakítani, s utolsó lépésként a fogadó oldalon a JSON objektumot fel kell dolgozni, tehát gyakorlatilag JSON-nal csak JSON objektumot lehet teljes értékűen szerializálni.
Ha csak JsonSerializable van, az olyan, mintha egy pendrive-ra rámásolnál mindent, aztán alacsony szintű hozzáféréssel letörölnéd a FAT táblát. Az adatok ott vannak, és a mintázatból sok esetben kitalálható, hogy mit akart a felhasználó, de ahhoz, hogy igazán használható legyen, mellékelni kell a leírót is.
Bocs, ha túl élesen fogalmaztam, semmiképpen ne vedd a személyed elleni támadásnak, és mindemellett én is örvendetes kezdeményezésnek tartom a JsonSerializable-t.
hello, nem vettem a
nem vettem a "személyem elleni támadásnak", örülök hogy bővebben kifejtetted.
A "félig" szót én úgy értettem, hogy a kódolás és dekódolás folyamatai közül az egyik esetében már megoldott a probléma. Persze gyakorlati használat (fejlesztés) közben ezzel így önmagában nem megyünk sokra, ebben nyilván igazad van.
na de sztem túl van tárgyalva a téma.
Hát ha objektumokkal akarunk
terminológia
a szerializációra valószínűleg a sorosítás a legmegfelelőbb magyar szó. Mindenesetre én egyáltalán nem vagyok híve a magyar szavak erőltetett használatának, úgyhogy maradok a szeralizációnál
Az "-able" végződés
ám gyakran egyszerűbb a json
Rekurzió?
Hasonló feladaton már én is agyaltam, magam részéről a feladatot két részben egyszerűsítettem:
1. nem akarok mindent szerializálni, csak a saját komponenseimet. (minden komponens tudta magáról, hogy őt hogyan kell szerializálni)
2. a kapott JSON-ban egy meghatározott attribútum jelenti a típust.
Amúgy a Date toJSON-ja ennél a megoldásnál elegendő, mert nem a dátumot alakítjuk vissza, hanem pl. a DateField komponenst, aki tudja, hogy ha a value érték string, akkor azon alakítani kell.
Egyszerűen
A JSON olyasmi, mint egy object literal-nal létrehozott JavaScript objektum, ebben pedig (a Mozilla kivételével) nem lehet körkörös hivatkozás. Ahhoz, hogy ilyen előfordulhasson, több lépést kell tenni. A szerializációnál ugyanezeket a lépéseket kell végigjárni.
Vegyünk egy példát: van egy leegyszerűsített DOM fánk, amit a gyökér elemével azonosítunk (mint általában minden fát). Az elemeknek van név tulajdonsága, ismerjük a gyermekeit és bármelyik elemtől el tudunk jutni a gyökérig a szülőkön keresztül. Itt ugye körkörös hivatkozás van, hiszen a gyerek hivatkozik a szülőre, a szülő hivatkozik a gyerek(ek)re. Ahogy a DOM-ban megszoktuk, a relációt az
appendChild()
metódussal tudjuk beállítani, ennek megfelelően elég csak a gyerekeket eltárolni.UTF-8
Pár éve még nem volt jó, most nem tudom mi a helyzet.
Mi négy éve használjuk gond
nem tudom mit ertesz jo
Tyrael
Csak úgy működnek jól