Utilisation de la fonctionnalité JSON native

ActionScript 3.0 fournit une API native permettant de coder et de décoder des objets ActionScript à l’aide du format JavaScript Object Notation (JSON). La classe JSON et les fonctions membres associées suivent la spécification ECMA-262 5e édition avec de légères variations.

Présentation de l’API JSON

L’API JSON ActionScript est constituée de la classe JSON et des fonctions membres toJSON() sur quelques classes natives. Pour les applications nécessitant un codage JSON personnalisé pour une classe, la structure ActionScript propose diverses méthodes permettant de remplacer le codage par défaut.

La classe JSON gère en interne l’importation et l’exportation des classes ActionScript qui ne fournissent pas de membre toJSON(). Dans ces cas, la classe JSON traverse les propriétés publiques de chaque objet qu’elle détecte. Si un objet contient d’autres objets, JSON parcourt les objets imbriqués et effectue la même traversée. Si un objet fournit une méthode toJSON(), JSON utilise cette méthode personnalisée plutôt que son algorithme interne.

L’interface JSON est composée d’une méthode d’encodage, stringify(), et d’une méthode de décodage, parse(). Chacune de ces méthodes fournit un paramètre permettant d’insérer votre propre logique dans la procédure de codage et de décodage JSON. Pour stringify(), ce paramètre est appelé replacer ; pour parse(), il est appelé reviver. Ces paramètres prennent une définition de fonction avec deux arguments à l’aide de la signature suivante :

function(k, v):*

Méthodes toJSON()

La signature des méthodes toJSON() est

public function toJSON(k:String):*

JSON.stringify() appelle la méthode toJSON(), si elle existe, pour chaque propriété publique qu’elle rencontre lorsqu’elle traverse un objet. Une propriété consiste en une paire clé-valeur. Lorsque stringify() appelle la méthode toJSON(), il transmet la clé, k, de la propriété qu’il examine actuellement. Une implémentation toJSON() standard évalue chaque nom de propriété et renvoie l’encodage souhaité de sa valeur.

La méthode toJSON() peut renvoyer tout type de valeur (identifiée à l’aide du signe *) et pas uniquement une chaîne. Ce type de renvoi de variable permet à la méthode toJSON() de renvoyer un objet, le cas échéant. Par exemple, si une propriété de votre classe personnalisée contient un objet issu d’une autre bibliothèque tierce, vous pouvez renvoyer cet objet dès que la méthode toJSON() détecte votre propriété. JSON parcourt alors l’objet tiers. Le processus de codage est le suivant :

  • Si la méthode toJSON() renvoie un objet qui n’est pas évalué par rapport à une chaîne, stringify() parcourt cet objet.

  • Si la méthode toJSON() renvoie une chaîne, stringify() enveloppe la valeur dans une autre chaîne, renvoie la chaîne enveloppée et passe à la valeur suivante.

Dans de nombreux cas, il est préférable de renvoyer un objet plutôt que de renvoyer une chaîne JSON créée par votre application. Le renvoi d’un objet implique l’utilisation de l’algorithme de codage JSON et permet à JSON de se répéter dans les objets imbriqués.

La méthode toJSON() n’est pas définie dans la classe Object ou dans la plupart des autres classes natives. Son absence indique à JSON d’effectuer sa traversée standard sur les propriétés publiques de l’objet. Si vous préférez, vus pouvez également utiliser la méthode toJSON() pour exposer les propriétés privées de votre objet.

Certaines classes natives posent néanmoins des problèmes que les bibliothèques ActionScript sont incapables de résoudre efficacement dans tous les cas d’utilisation. Pour ces classes, ActionScript fournit une implémentation triviale que le client peut à nouveau implémenter selon ses besoins. Les classes qui fournissent des membres toJSON() triviaux sont les suivantes :

  • ByteArray

  • Date

  • Dictionary

  • XML

Vous pouvez intégrer la classe ByteArray dans une sous-classe pour remplacer sa méthode toJSON(), mais vous pouvez aussi redéfinir son prototype. Les classes Date et XML, qui sont déclarées comme étant finales, vous obligent à utiliser le prototype de classe pour redéfinir toJSON(). La classe Dictionary est déclarée comme étant dynamique, ce qui vous donne la liberté de remplacer la méthode toJSON().

Définition du comportement JSON personnalisé

Vous disposez de plusieurs options pour implémenter vos propres codage et décodage JSON pour les classes natives :

  • Définir ou remplacer toJSON() sur la sous-classe personnalisée d’une classe native non finale

  • Définir et redéfinir toJSON() sur le prototype de la classe

  • Définir une propriété toJSON sur une classe dynamique

  • Utiliser les paramètres JSON.stringify() replacer et JSON.parser() reviver

Définition de la méthode toJSON() sur le prototype d’une classe intégrée

L’implémentation JSON native dans ActionScript imite le mécanisme JSON ECMAScript défini dans ECMA-262, 5e édition. Etant donné qu’ECMAScript ne prend pas en charge les classes, ActionScript définit le comportement de JSON en termes de distribution basée sur les prototypes. Ancêtres des classes ActionScript 3.0, les prototypes permettent un héritage simulé, ainsi que les ajouts et redéfinitions de membres.

ActionScript permet de définir ou de redéfinir toJSON() sur le prototype d’une classe. Ce privilège s’étend aux classes déclarées comme étant finales. Lorsque vous définissez toJSON() sur le prototype d’une classe, votre définition s’applique à toutes les occurrences de cette classe dans le cadre de votre application. Par exemple, voici comment vous pouvez définir une méthode toJSON() sur le prototype de la classe MovieClip :

MovieClip.prototype.toJSON = function(k):* { 
    trace("prototype.toJSON() called."); 
    return "toJSON"; 
} 

Lorsque votre application appelle la méthode stringify() sur une occurrence de MovieClip, stringify()renvoie le résultat de votre méthode toJSON() :

var mc:MovieClip = new MovieClip(); 
var js:String = JSON.stringify(mc); //"prototype toJSON() called." 
trace("js: " + js); //"js: toJSON"

Vous pouvez en outre remplacer toJSON() dans les classes natives qui définissent cette méthode. Par exemple, le code suivant remplace Date.toJSON() :

Date.prototype.toJSON = function (k):* { 
    return "any date format you like via toJSON: "+ 
        "this.time:"+this.time + " this.hours:"+this.hours; 
} 
var dt:Date = new Date(); 
trace(JSON.stringify(dt)); 
// "any date format you like via toJSON: this.time:1317244361947 this.hours:14" 

Définition ou remplacement de toJSON() au niveau de la classe

Les applications n’ont pas toujours besoin d’utiliser des prototypes pour redéfinir toJSON(). Il est également possible de définir toJSON() en tant que membre d’une sous-classe si la classe parente n’est pas marquée comme finale. Vous pouvez par exemple étendre la classe ByteArray et définir une fonction toJSON() publique :

package { 
 
    import flash.utils.ByteArray; 
    public class MyByteArray extends ByteArray 
    { 
        public function MyByteArray() { 
        } 
         
        public function toJSON(s:String):* 
        { 
            return "MyByteArray"; 
        } 
 
    } 
} 
 
 
var ba:ByteArray = new ByteArray(); 
trace(JSON.stringify(ba)); //"ByteArray" 
var mba:MyByteArray = new MyByteArray(); //"MyByteArray" 
trace(JSON.stringify(mba)); //"MyByteArray"

Si un classe est dynamique, il est possible d’ajouter une propriété toJSON à un objet de cette classe et de lui attribuer une fonction de la façon suivante :

var d:Dictionary = new Dictionary(); 
trace(JSON.stringify((d))); // "Dictionary" 
d.toJSON = function(){return {c : "toJSON override."};} // overrides existing function 
trace(JSON.stringify((d))); // {"c":"toJSON override."}

Vous pouvez remplacer, définir ou redéfinir toJSON() sur n’importe quelle classe ActionScript. Néanmoins, la plupart des classes ActionScript intégrées ne définissent pas toJSON(). La classe Object ne définit pas la méthode toJSON dans son prototype par défaut ni ne la déclare en tant que membre de classe. Seule une poignée de classes natives définit la méthode comme fonction prototype. C’est pourquoi, dans la plupart des cas, vous ne pouvez pas remplacer toJSON() de façon traditionnelle.

Les classes natives qui ne définissent pas toJSON() sont sérialisées sur JSON par l’implémentation JSON interne. Dans la mesure du possible, évitez de remplacer cette fonctionnalité intégrée. Si vous définissez un membre toJSON(), la classe JSON utilise votre logique plutôt que sa propre fonctionnalité.

Utilisation du paramètre replacer de la méthode JSON.stringify()

Il peut être utile de remplacer toJSON() sur le prototype en vue de modifier le comportement d’exportation JSON d’une classe dans une application. Néanmoins, votre logique d’exportation doit s’appliquer uniquement à des cas spéciaux sous des conditions provisoires. Pour prendre en compte ces modifications à petite échelle, vous pouvez utiliser le paramètre replacer de la méthode JSON.stringify().

La méthode stringify() applique la fonction transmise via le paramètre replacer à l’objet en cours de codage. La signature pour cette fonction est similaire à celle de toJSON() :

function (k,v):* 

Contrairement à la méthode toJSON(), la fonction replacer requiert la valeur v, ainsi que la clé k. Cette différence est nécessaire, car la méthode stringify() est définie sur l’objet JSON statique et non sur l’objet en cours de codage. Lorsque la méthode JSON.stringify() appelle replacer(k,v), elle traverse l’objet d’entrée d’origine. Le paramètre implicite this transmis à la fonction replacer fait référence à l’objet qui détient la clé et la valeur. Etant donné que la méthode JSON.stringify() ne modifie pas l’objet d’entrée d’origine, cet objet reste inchangé dans le conteneur actuellement traversé. Vous pouvez par conséquent utiliser le code this[k] pour interroger la clé sur l’objet d’origine. Le paramètre v renferme la valeur que toJSON() convertit.

Tout comme toJSON(), la fonction replacer peut renvoyer tout type de valeur. Si replacer renvoie une chaîne, le moteur JSON convertit le contenu en séquence d’échappement entre guillemets et place ce contenu également entre guillemets. Cette structure garantit que stringify() reçoive un objet de chaîne JSON valide qui reste une chaîne dans un prochain appel de JSON.parse().

Le code suivant utilise le paramètre replacer et le paramètre this implicite pour renvoyer les valeurs time et hours d’un objet Date :

JSON.stringify(d, function (k,v):* { 
    return "any date format you like via replacer: "+ 
        "holder[k].time:"+this[k].time + " holder[k].hours:"+this[k].hours; 
});

Utilisation du paramètre reviver de la méthode JSON.parse()

Le paramètre reviver de la méthode JSON.parse() est l’opposé de la fonction replacer : il convertit une chaîne JSON en objet ActionScript utilisable. L’argument reviver est une fonction qui prend deux paramètres et renvoie tout type :

function (k,v):* 

Dans cette fonction, k est une clé et v est la valeur de k. Tout comme stringify(), parse() traverse les paires clé-valeur JSON et applique la fonctionreviver, si elle existe, à chaque paire. L’un des problèmes potentiels est que la classe JSON ne renvoie pas le nom de classe ActionScript d’un objet. Il peut par conséquent être difficile de savoir quel type d’objet ranimer. Ce problème peut en outre s’avérer particulièrement délicat lorsque les objets sont imbriqués. En désignant les fonctions toJSON(), replacer et reviver, vous pouvez trouver des moyens d’identifier les objets ActionScript exportés tout en gardant intacts les objets d’origine.

Exemple d’analyse

L’exemple suivant illustre une stratégie de ranimation d’objets analysés à partir de chaînes JSON. Cet exemple définit deux classes : JSONGenericDictExample et JSONDictionaryExtnExample. La classe JSONGenericDictExample est une classe Dictionary personnalisée. Chaque enregistrement contient le nom et la date de naissance d’une personne, ainsi qu’un ID unique. Chaque fois que le constructeur JSONGenericDictExample est appelé, il ajoute l’objet nouvellement créé à un tableau statique interne avec un entier augmentant statiquement comme son ID. La classe JSONGenericDictExample définit également une méthode revive() qui extrait uniquement l’entier du membre id le plus long. La méthode revive() utilise cet entier pour rechercher et renvoyer l’objet ranimable adéquat.

La classe JSONDictionaryExtnExample étend la classe Dictionary ActionScript. Ses enregistrements n’ont pas de structure définie et peuvent contenir toutes sortes de données. Les données sont attribuées après la construction de l’objet JSONDictionaryExtnExample et non par les propriétés définies dans la classe. Les enregistrements de JSONDictionaryExtnExample utilisent les objets JSONGenericDictExample comme clés. Lorsqu’un objet JSONDictionaryExtnExample est ranimé, la fonction JSONGenericDictExample.revive() utilise l’ID associé à JSONDictionaryExtnExample pour récupérer l’objet de clé correct.

Plus important encore, la méthode JSONDictionaryExtnExample.toJSON() renvoie une chaîne de marqueurs en plus de l’objet JSONDictionaryExtnExample. Cette chaîne identifie la sortie JSON comme appartenant à la classe JSONDictionaryExtnExample. Ce marqueur indique clairement le type d’objet en cours de traitement lors de l’exécution de JSON.parse().

package { 
    // Generic dictionary example: 
    public class JSONGenericDictExample { 
        static var revivableObjects = []; 
        static var nextId = 10000; 
        public var id; 
        public var dname:String; 
        public var birthday; 
 
        public function JSONGenericDictExample(name, birthday) { 
            revivableObjects[nextId] = this; 
            this.id       = "id_class_JSONGenericDictExample_" + nextId; 
            this.dname     = name; 
            this.birthday = birthday; 
            nextId++; 
        } 
        public function toString():String { return this.dname; } 
        public static function revive(id:String):JSONGenericDictExample { 
            var r:RegExp = /^id_class_JSONGenericDictExample_([0-9]*)$/; 
            var res = r.exec(id); 
            return JSONGenericDictExample.revivableObjects[res[1]]; 
        } 
    } 
} 
 
package { 
    import flash.utils.Dictionary; 
    import flash.utils.ByteArray; 
 
    // For this extension of dictionary, we serialize the contents of the 
    // dictionary by using toJSON 
    public final class JSONDictionaryExtnExample extends Dictionary { 
        public function toJSON(k):* { 
            var contents = {}; 
            for (var a in this) { 
                contents[a.id] = this[a]; 
            } 
     
            // We also wrap the contents in an object so that we can 
            // identify it by looking for the marking property "class E" 
            // while in the midst of JSON.parse. 
            return {"class JSONDictionaryExtnExample": contents}; 
        } 
 
        // This is just here for debugging and for illustration 
        public function toString():String { 
            var retval = "[JSONDictionaryExtnExample <"; 
            var printed_any = false; 
            for (var k in this) { 
                retval += k.toString() + "=" + 
                "[e="+this[k].earnings + 
                ",v="+this[k].violations + "], " 
                printed_any = true; 
            } 
            if (printed_any) 
                retval = retval.substring(0, retval.length-2); 
            retval += ">]" 
            return retval; 
        } 
    } 
} 

Lorsque le script d’exécution suivant appelle JSON.parse() sur un objet JSONDictionaryExtnExample, la fonction reviver appelle JSONGenericDictExample.revive() sur chaque objet dans JSONDictionaryExtnExample. Cet appel extrait l’ID qui représente la clé de l’objet. La fonction JSONGenericDictExample.revive() utilise cet ID pour récupérer et renvoyer l’objet JSONDictionaryExtnExample stocké à partir d’un tableau statique privé.

import flash.display.MovieClip; 
import flash.text.TextField; 
 
var a_bob1:JSONGenericDictExample = new JSONGenericDictExample("Bob", new Date(Date.parse("01/02/1934"))); 
var a_bob2:JSONGenericDictExample = new JSONGenericDictExample("Bob", new Date(Date.parse("05/06/1978"))); 
var a_jen:JSONGenericDictExample = new JSONGenericDictExample("Jen", new Date(Date.parse("09/09/1999"))); 
 
var e = new JSONDictionaryExtnExample(); 
e[a_bob1] = {earnings: 40, violations: 2}; 
e[a_bob2] = {earnings: 10, violations: 1}; 
e[a_jen]  = {earnings: 25, violations: 3}; 
 
trace("JSON.stringify(e): " + JSON.stringify(e)); // {"class JSONDictionaryExtnExample": 
                        //{"id_class_JSONGenericDictExample_10001": 
                        //{"earnings":10,"violations":1}, 
                        //"id_class_JSONGenericDictExample_10002": 
                        //{"earnings":25,"violations":3}, 
                        //"id_class_JSONGenericDictExample_10000": 
                        // {"earnings":40,"violations":2}}} 
 
var e_result = JSON.stringify(e); 
 
var e1 = new JSONDictionaryExtnExample(); 
var e2 = new JSONDictionaryExtnExample(); 
 
// It's somewhat easy to convert the string from JSON.stringify(e) back 
// into a dictionary (turn it into an object via JSON.parse, then loop 
// over that object's properties to construct a fresh dictionary). 
// 
// The harder exercise is to handle situations where the dictionaries 
// themselves are nested in the object passed to JSON.stringify and 
// thus does not occur at the topmost level of the resulting string. 
// 
// (For example: consider roundtripping something like 
//   var tricky_array = [e1, [[4, e2, 6]], {table:e3}] 
// where e1, e2, e3 are all dictionaries.  Furthermore, consider 
// dictionaries that contain references to dictionaries.) 
// 
// This parsing (or at least some instances of it) can be done via 
// JSON.parse, but it's not necessarily trivial.  Careful consideration 
// of how toJSON, replacer, and reviver can work together is 
// necessary. 
 
var e_roundtrip = 
    JSON.parse(e_result, 
               // This is a reviver that is focused on rebuilding JSONDictionaryExtnExample objects. 
               function (k, v) { 
                    if ("class JSONDictionaryExtnExample" in v) { // special marker tag; 
                        //see JSONDictionaryExtnExample.toJSON(). 
                       var e = new JSONDictionaryExtnExample(); 
                       var contents = v["class JSONDictionaryExtnExample"]; 
                       for (var i in contents) { 
                          // Reviving JSONGenericDictExample objects from string 
                          // identifiers is also special; 
                          // see JSONGenericDictExample constructor and 
                          // JSONGenericDictExample's revive() method. 
                           e[JSONGenericDictExample.revive(i)] = contents[i]; 
                       } 
                       return e; 
                   } else { 
                       return v; 
                   } 
               }); 
 
trace("// == Here is an extended Dictionary that has been round-tripped  =="); 
trace("// == Note that we have revived Jen/Jan during the roundtrip.    =="); 
trace("e:           " + e); //[JSONDictionaryExtnExample <Bob=[e=40,v=2], Bob=[e=10,v=1], 
                           //Jen=[e=25,v=3]>] 
trace("e_roundtrip: " + e_roundtrip); //[JSONDictionaryExtnExample <Bob=[e=40,v=2], 
                                     //Bob=[e=10,v=1], Jen=[e=25,v=3]>] 
trace("Is e_roundtrip a JSONDictionaryExtnExample? " + (e_roundtrip is JSONDictionaryExtnExample)); //true 
trace("Name change: Jen is now Jan"); 
a_jen.dname = "Jan" 
 
trace("e:           " + e); //[JSONDictionaryExtnExample <Bob=[e=40,v=2], Bob=[e=10,v=1], 
                           //Jan=[e=25,v=3]>] 
trace("e_roundtrip: " + e_roundtrip); //[JSONDictionaryExtnExample <Bob=[e=40,v=2], 
                                     //Bob=[e=10,v=1], Jan=[e=25,v=3]>]