ネイティブ JSON 機能の使用

ActionScript 3.0 には、JavaScript Object Notation(JSON)形式を使用して ActionScript オブジェクトのエンコードおよびデコードを行うためのネイティブ API が用意されています。JSON クラスおよびサポートするメンバー関数は、ECMA-262 第 5 版の仕様にほぼ従っています。

JSON API の概要

ActionScript JSON API は、JSON クラスと、いくつかのネイティブクラスの toJSON() メンバー関数から構成されています。クラスのカスタム JSON エンコードを必要とするアプリケーション向けに、ActionScript にはデフォルトのエンコーディングをオーバーライドする方法が用意されています。

JSON クラスは、 toJSON() メンバーの用意されていないすべての ActionScript クラスについて、読み込みと書き出しを内部的に処理します。そのために、JSON は、検出した各オブジェクトのパブリックプロパティをスキャンします。オブジェクトに他のオブジェクトが含まれる場合、ネストされたオブジェクトを再帰的に処理し、同様にスキャンします。オブジェクトに toJSON() メソッドが含まれる場合、JSON では内部アルゴリズムの代わりにそのカスタムメソッドが使用されます。

JSON インターフェイスはエンコードメソッド stringify() とデコードメソッドの parse() で構成されます。これらのメソッドにはそれぞれ、JSON のエンコードまたはデコードワークフローに独自のロジックを挿入できるパラメーターが用意されています。 stringify() では、このパラメーターは replacer です。 parse() では、 reviver です。これらのパラメーターは、次の署名を使用する 2 つの引数を持つ関数定義を持っています。

function(k, v):*

toJSON() メソッド

toJSON() メソッドの署名は、次のとおりです。

public function toJSON(k:String):*

JSON.stringify() は、オブジェクトをスキャンして検出した各パブリックプロパティに toJSON() が存在する場合は、それを呼び出します。プロパティはキーと値のペアで構成されます。 stringify() toJSON() を呼び出すときに、現在調べているプロパティのキーである k を渡します。通常の toJSON() 実装は各プロパティ名を評価し、その値に必要となるエンコーディングを返します。

toJSON() メソッドは、String だけでなくすべての型(* で表す)の値を返すことができます。変数の戻り値の型が任意なので、状況に応じて toJSON() でオブジェクトを返すことができます。例えば、カスタムクラスのプロパティに他のサードパーティライブラリのオブジェクトが含まれる場合、 toJSON() がプロパティを検出したときにそのオブジェクトを返すようにできます。その後、JSON はそのサードパーティオブジェクト内で再帰的に処理します。エンコードの処理フローは以下のとおりです。

  • toJSON() が文字列として評価されないオブジェクトを返す場合、 stringify() はそのオブジェクト内で再帰的に処理します。

  • toJSON() が文字列を返す場合、 stringify() はその値を別の文字列内にラップし、ラップされた文字列を返してから次の値に移ります。

多くの場合、アプリケーションで作成される JSON 文字列を返すためにオブジェクトを返すことをお勧めします。オブジェクトを返す場合、ビルトイン JSON エンコードアルゴリズムが使用され、JSON はネストされたオブジェクトに再帰できます。

toJSON() メソッドは、Object クラス、または他の大部分のネイティブクラスには定義されていません。定義されていないことにより、JSON は、オブジェクトのパブリックプロパティに対して標準のスキャンを行うように指示されます。 toJSON() を使用して、オブジェクトのプライベートプロパティを公開することもできます。

いくつかのネイティブクラスでは、すべての使用ケースについて ActionScript ライブラリで効率的に解決できない問題もあります。これらのクラスに対しては、ActionScript で一般的な実装が用意されており、ユーザーはニーズに合わせてそれらを再実装できます。一般的な toJSON() メンバーを提供するクラスには、次のようなものがあります。

  • ByteArray

  • Date

  • Dictionary

  • XML

ByteArray クラスをサブクラスにして、その toJSON() メソッドをオーバーライドするか、そのプロトタイプを再定義することができます。最後に宣言される Date クラスおよび XML クラスは、 toJSON() を再定義するためにクラスプロトタイプを使用する必要があります。Dictionary クラスは動的に宣言されるため、 toJSON() をオーバーライドする際の自由度が増します。

カスタム JSON 動作の定義

ネイティブクラスの独自の JSON エンコードおよびデコードを実装するには、いくつかのオプションから選択できます。

  • 最終以外のネイティブクラスのカスタムサブクラスの toJSON() を定義またはオーバーライドします

  • クラスプロトタイプの toJSON() を定義または再定義します

  • 動的クラスの toJSON プロパティを定義します

  • JSON.stringify() replacer および JSON.parser() reviver パラメーターを使用します

ビルトインクラスのプロトタイプでの toJSON() の定義

ActionScript でのネイティブ JSON 実装は、ECMA-262 第 5 版で定義されている ECMAScript JSON メカニズムを反映しています。ECMAScript はクラスをサポートしていないので、ActionScript では、プロトタイプベースの送出という観点で JSON の動作を定義しています。プロトタイプは ActionScript 3.0 クラスに先行する位置づけにあり、継承のシミュレートと、メンバーの追加および再定義が可能です。

ActionScript では、任意のクラスのプロトタイプに toJSON() を定義または再定義できます。この特権は、最終とマークされているクラスに対しても適用されます。クラスのプロトタイプで toJSON() を定義すると、この定義はアプリケーションのスコープ内にあるそのクラスのすべてのインスタンスに適用されます。例えば、MovieClip プロトタイプで toJSON() メソッドを定義する方法は次のとおりです。

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

さらに、アプリケーションが MovieClip インスタンスの stringify() を呼び出すと、 stringify() toJSON() メソッドの出力を返します。

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

メソッドを定義しているネイティブクラスの toJSON() をオーバーライドすることもできます。例えば、次のコードでは 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" 

toJSON() のクラスレベルでの定義またはオーバーライド

アプリケーションでは toJSON() を再定義するために必ずしもプロトタイプを使用する必要はありません。親クラスが最終とマークされていない場合は、 toJSON() をサブクラスのメンバーとして定義することもできます。例えば、ByteArray クラスを拡張してパブリックの toJSON() 関数を定義できます。

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"

クラスが動的な場合は、次のようにそのクラスのオブジェクトに toJSON プロパティを追加し、それに関数を割り当てることもできます。

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

ActionScript クラスでは toJSON() をオーバーライド、定義または再定義できます。ただし、ほとんどのビルトイン ActionScript クラスでは toJSON() は定義されていません。Object クラスでは toJSON をデフォルトのプロトタイプ内で定義しておらず、またクラスメンバーとして宣言していません。ネイティブクラスでこのメソッドをプロトタイプ関数として定義しているのはほんの少数です。そのため、ほとんどのクラスでは従来の感覚で toJSON() をオーバーライドすることはできません。

toJSON() を定義しないネイティブクラスは、内部 JSON 実装によって JSON に直列化されます。この組み込み機能は、できれば置き換えないでください。 toJSON() メンバーを定義すると、JSON クラスでは独自の機能ではなくユーザーのロジックが使用されます。

JSON.stringify() replacer パラメーターの使用

プロトタイプで toJSON() をオーバーライドすると、アプリケーション全体でクラスの JSON 書き出し動作を変更することができます。ただし、場合によっては、書き出しのロジックが一時的な条件下の特殊な場合にのみ適用されることもあります。このような小さなスコープでの変更に対応するには、 JSON.stringify() メソッドの replacer パラメーターを使用できます。

stringify() メソッドは、 replacer パラメーターを通じて渡された関数を、エンコードされるオブジェクトに適用します。この関数の署名は、 toJSON() の署名に似ています。

function (k,v):* 

toJSON() とは異なり、 replacer 関数にはキー k に加えて、値 v が必要です。この違いが必要なのは、エンコードされるオブジェクトではなく JSON オブジェクトで stringify() が定義されるためです。 JSON.stringify() replacer(k,v) を呼び出すのは、元の入力オブジェクトがスキャンされている段階です。 replacer 関数に暗黙的な this パラメーターを渡すと、キーと値を持つそのオブジェクトが参照されます。 JSON.stringify() は元の入力オブジェクトを変更しないので、そのオブジェクトはスキャンされているコンテナ内で変更されないままの状態です。そのため、コード this[k] を使用して、元のオブジェクト上のキーを問い合わせることができます。 v パラメーターには、 toJSON() が変換する値が格納されます。

toJSON() と同様に、 replacer 関数は任意の型の値を返すことができます。 replacer がストリングを返す場合、JSON エンジンはアカウントを引用符で囲んでエスケープしてから、そのエスケープされたコンテンツも引用符で囲みます。このように囲むことで、 stringify() は、その後の JSON.parse() の呼び出しでストリングを残す有効な JSON ストリングオブジェクトを受け取ることが保証されます。

次のコードは replacer パラメーターおよび暗黙的な this パラメーターを使用して、Date オブジェクトの time 値および hours 値を返します。

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; 
});

JSON.parse() の reviver パラメーターの使用

JSON.parse() メソッドの reviver パラメーターは、 replacer 関数と反対の動作を行います。すなわち、JSON ストリングを、使用可能な ActionScript オブジェクトに変換します。 reviver 引数は、2 つのパラメーターを持ち、任意の型を返す次のような関数です。

function (k,v):* 

この関数で、 k はキー、 v k の値です。 stringify() と同様に、 parse() は JSON のキーと値のペアをスキャンし、 reviver 関数が存在する場合は、それを各ペアに適用します。潜在的な問題としては、JSON クラスがオブジェクトの ActionScript クラス名を出力しないということがあります。したがって、回復するオブジェクトの型を認識することが難しい場合があります。この問題は、オブジェクトがネストしている場合に、特に問題を引き起こす可能性があります。 toJSON() replacer および reviver 関数を設計する段階で、元のオブジェクトをそのまま保ちつつ、書き出される ActionScript オブジェクトを識別する方法を工夫することができます。

解析の例

次の例は、JSON ストリングから解析したオブジェクトを回復する方法を示します。この例では、2 つのクラス JSONGenericDictExample と JSONDictionaryExtnExample を定義しています。クラス JSONGenericDictExample は、カスタムのディクショナリクラスです。各レコードには、個人の名前、誕生日、一意の ID が含まれています。JSONGenericDictExample コンストラクターが呼び出されるたびに、新しく作成されたオブジェクトが内部の静的な配列に追加されます。このとき、静的にインクリメントされる整数も ID として一緒に追加されます。クラス JSONGenericDictExample は、長い id メンバーから整数部分だけを抽出する revive() メソッドも定義します。 revive() メソッドは、この整数を使用して検索を行い、回復可能な正しいオブジェクトを返します。

クラス JSONDictionaryExtnExample は、ActionScript Dictionary クラスを拡張します。このレコードは、構造が設定されておらず、どのようなデータも格納できません。データは、クラス定義として割り当てられるのではなく、JSONDictionaryExtnExample オブジェクトが作成された後で割り当てられます。JSONDictionaryExtnExample レコードは、JSONGenericDictExample オブジェクトをキーとして使用します。JSONDictionaryExtnExample オブジェクトが回復すると、 JSONGenericDictExample.revive() 関数は JSONDictionaryExtnExample に関連付けられている ID を 使用して、正しいキーオブジェクトを取得します。

最も重要なこととして、 JSONDictionaryExtnExample.toJSON() メソッドは JSONDictionaryExtnExample オブジェクトに加えてマーカーストリングも返します。このストリングでは、JSON の出力は JSONDictionaryExtnExample クラスに属するものと認識されます。このマーカーにより、 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; 
        } 
    } 
} 

次のランタイムスクリプトが JSONDictionaryExtnExample オブジェクトで JSON.parse() を呼び出すと、 reviver 関数が JSONDictionaryExtnExample 内の各オブジェクトについて JSONGenericDictExample.revive() を呼び出します。この呼び出しはオブジェクトキーとなる ID を抽出します。 JSONGenericDictExample.revive() 関数は、この ID を使用し、保存された JSONDictionaryExtnExample オブジェクトをプライベートな静的配列から取得して返します。

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