ugrás a tartalomhoz

JSON, az alapokon túl

presidento · 2011. Nov. 15. (K), 01.27
Ú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 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
A szemfülesebbek észrevehetik, hogy kicsit csaltam: a specifikációban a 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);
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 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.
 
1

A PHP-s megoldások közül egy

Crystal · 2011. Nov. 15. (K), 13.51
A PHP-s megoldások közül egy sem


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.
2

Ezzel nem leszünk sokkal előrébb

presidento · 2011. Nov. 15. (K), 15.27
hiszen a hivatkozott cikk is írja:
Now almost certainly somebody will ask "and what about the other way round?" - The only answer there is: Sorry there we can't do much. JSON doesn't encode and meta-information so our generic parser in json_decode() can't do anything special.


Amíg nincs reviver(), addig az adatokat csak elkódolni tudjuk, visszanyerni viszont nem...
4

szerintem pontosan ezt írtam,

Crystal · 2011. Nov. 15. (K), 17.33
szerintem pontosan ezt írtam, nem értem, mibe sikerült belekötnöd...
ami a replacer-nek megfelel, szóval félig már jók vagyunk/leszünk

Egyébként gratulálok a cikkhez, szépen összeszedted :)
5

Nem kötekedés képpen… :)

presidento · 2011. Nov. 15. (K), 23.06
A „félig” szóba sikerült belekötnöm. Ugyanis ha az adatokat csak elmenteni tudjuk, de visszaolvasni nem, az még messze nem fél megoldás. A biztonsági mentés a visszaállítás után tekinthető sikeresnek, anélkül nem sokat ér.

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:
This method is generally intended to, by default, usefully serialize Date objects during JSON serialization.

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.
6

hello, nem vettem a

Crystal · 2011. Nov. 16. (Sze), 13.25
hello,

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.
7

Hát ha objektumokkal akarunk

inf3rno · 2011. Nov. 16. (Sze), 19.42
Hát ha objektumokkal akarunk dolgozni, akkor kellene egy ClassMap szerver oldalra, meg kellene validálás is. JSON-t gondolom kicsomagolás után szokták validálni...

JSON stringbe szerializálsz (van erre magyar szó?)
- JSON-ná alakítasz, stb... ugye transzformációról van szó, úgyhogy bármit lehet használni, ami illik ebbe a kontextusba. Nekem a JSON-ba csomagolsz tetszik a legjobban most, gondolom ez már a Karácsony előszele... :D
9

terminológia

Crystal · 2011. Nov. 17. (Cs), 00.25
- JSON-ná alakítasz, stb

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
3

Az "-able" végződés

inf3rno · 2011. Nov. 15. (K), 16.00
Ugye a főnevek osztályok, az igék metódusok, a melléknevek meg interface-ek. Szóval ez alapján interface kell, hogy legyen, bár a php-nél sosem lehet tudni... :S
15

ám gyakran egyszerűbb a json

james88 · Szep. 13. (Sze), 05.02
ám gyakran egyszerűbb a json formatter menni segítségért.
8

Rekurzió?

T.G · 2011. Nov. 16. (Sze), 20.41
Szia, rekurziót hogy kezelnéd le?

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.

var panel = new Ext.Panel({
    title: "Szia világ!",
    width: 100
});
panel.setWidth(200);

var panelConfig = panel.toJSON();
// {"xtype":"panel","title":"Szia világ!","width":200}

var panel2 = Ext.create(panelConfig);
A probléma ott jött elő, amikor két komponens egymásra hivatkozik. Azt még csak-csak megoldottam, hogy ne legyen végtelen ciklus, bár azt sem szépen:), ám létrehozáskor az azért mégsem ugyanaz volt, az eredeti függőséget nem hozta vissza.

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.
10

Egyszerűen

presidento · 2011. Nov. 17. (Cs), 13.12
Pontosan ugyanúgy, ahogy JavaScript alatt kezeled a körkörös hivatkozást. :)

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.
function Node(name) {
    this.name = name;
    this.children = [];
};
Node.prototype.parentNode = null;
Node.prototype.children = null;
Node.prototype.appendChild = function(aChildNode) {
    this.children.push(aChildNode);
    aChildNode.parentNode = this;
};

Node.prototype.toJSON = function() {
    return {
        name: this.name,
        children: this.children
    };
};

Node.fromJSON = function(data) {
    var node = new Node(data.name);
    for (var i = 0; i < data.children.length; i++) {
        node.appendChild(data.children[i]);
    }
    return node;
};  

myJSON.addType('Node', Node);

////////////////////////////////////////////////////////////////////////////////  

var gp = new Node('grandparent');
var p = new Node('parent');
gp.appendChild(p);
var c1 = new Node('child1');
p.appendChild(c1);
var c2 = new Node('child2');
p.appendChild(c2);


var str = myJSON.stringify(gp);
var o = myJSON.parse(str);

console.log(gp.children[0].children[1].name); // child2
console.log(gp.children[0].children[1].parentNode.parentNode === gp); // true
console.log(str);
console.log(o.children[0].children[1].name); // chil2
console.log(o.children[0].children[1].parentNode.parentNode === o); // true
Lehetnek ennél bonyolultabb esetek is, de a lényeg ugyanaz: körkörös hivatkozást sosem egy lépésben hozol létre, és ezeket a lépéseket kell ismételni a szerializációkor is.
11

UTF-8

Szalbint · 2011. Nov. 26. (Szo), 13.27
Tényleg, a PHP-s JSON függvények jól működnek már utf-8 karakterkódolás mellett?

Pár éve még nem volt jó, most nem tudom mi a helyzet.
12

Mi négy éve használjuk gond

Hidvégi Gábor · 2011. Nov. 26. (Szo), 18.25
Mi négy éve használjuk gond nélkül utf8-cal.
13

nem tudom mit ertesz jo

Tyrael · 2011. Nov. 26. (Szo), 20.50
nem tudom mit ertesz jo mukodes alatt, evek ota hasznalom gond nelkul, de az 5.4-es verzioval erkezik egy szerintem hasznos fejlesztes:

Tyrael
14

Csak úgy működnek jól

tgr · 2011. Nov. 27. (V), 21.05
Csak úgy működnek jól, más kódolás esetén csendben eldobja, és üres string kerül a JSON objektum megfelelő paraméterébe. Kib****** idegesítő.