読者です 読者をやめる 読者になる 読者になる

SIN@SAPPOROWORKSの覚書

C#を中心に、夜な夜な試行錯誤したコードの記録です。

SwiftyJSONをクラスオブジェクトで初期化する(Swiftにおけるリフレクション)

1 SwiftyJSON

SwiftyJSONは、Swiftで簡単にJSONを扱うライブラリとして、各所で紹介されており、機能も非常に充実しています。
しかし、何故か、JSON文字列をパースする処理ばかりで、その逆のJSON文字列へのシリアライズは見あたりませんでした。

つまり

// サンプルのクラス
class Parson{
    var name:String // 名前
    var age:Int // 年齢
    init(name:String,age:Int){
        self.name = name
        self.age = age
    }
}
// クラスオブジェクトからJSONオブジェクトを生成する
var json:JSON = JSON.Parse(Parson(name: "TARO", age: 25))
// JSONテキストを吐く
println(json.rawString())

みたいに書いて、

{
       "age" : 25,
       "name" : "TARO"
}

と出力してほしいのです。

ちょっと、悩んでいたのですが・・・もしかすると、Swiftでは、リフレクションが正式に仕様化されていないようですので、それが理由なのかも知れません。

しかし、一応、リフレクションは可能なようで、いくつか紹介されている記事を見つけましたので、
今回は、踏ん張って、SwiftyJSONの拡張メソッドとして、JSON文字列へのシリアライズ(あくまで触りだけ)を書いてみます。

Swift初めて、約1ヶ月の超初心者で突っ込みどころ満載ですw

2 クラスオブジェクトのシリアライズ

とりあえず、最初のParsonクラスをシリアライズできるように書いてみました。
reflect() で、オブジェクトの名前や値、型などが取得できるので、個別にJSONオブジェクトを作成してディクショナリに追加しました。

extension JSON {
    static func Parse(data:Any) -> JSON{
        var json:JSON=[:]
        let ref = reflect(data)
        for var i = 0; i < ref.count; ++i {
            let (key, mirror) = ref[i]
            //とりあえず、String,Int,Boolのみ対応
            if let n = mirror.value as? String {
                json[key] = JSON(n)
            } else if let n = mirror.value as? Int{
                json[key] = JSON(n)
            } else if let n = mirror.value as? Bool{
                json[key] = JSON(n)
            }
        }
        return json
    }
}

// クラスのオブジェクトからJSONオブジェクトを生成する
var json:JSON = JSON.Parse(Parson(name: "TARO", age: 25))
// JSONテキストを吐く
textView.text = json.rawString() // 表示

f:id:furuya02:20150730034829p:plain

なんとか動いているようです。

3 オプショナル型と階層構造への対応

続いて、先ほどのParsonクラスが、さらにParson型のプロパティを持つ場合への対応を書いてみます。
なお、これは、オプショナル型で定義し、nilの場合も処理できるようにしました。

クラスは、次のように変化します。

class Parson{
    var name:String
    var age:Int
    var mother:Parson? // オプショナル型のParson(母親の情報)
    init(name:String,age:Int){
        self.name = name
        self.age = age
    }
}

そして、拡張メソッドは、こんな感じでしょうか

extension JSON {
    static func Parse(data:Any) -> JSON{
        var json:JSON=[:]
        let ref = reflect(data)
        for var i = 0; i < ref.count; ++i {
            let (key, mirror) = ref[i]
            //nil検出(やや強引ですがAny型がnilと比較できなかったので・・・)
            if mirror.disposition == .Optional && "\(mirror.value)" == "nil" {
                 json[key] = nil
            } else {
                //とりあえず、String,Int,Boolのみ対応
                if let n = mirror.value as? String {
                    json[key] = JSON(n)
                } else if let n = mirror.value as? Int{
                    json[key] = JSON(n)
                } else if let n = mirror.value as? Bool{
                    json[key] = JSON(n)
                } else {
                    let subref = reflect(mirror.value)
                    let (k, m) = subref[0]
                    json[key] = Parse(m.value) // 再帰で同じメソッドにぶっ込む
                }
            }
        }
        return json
    }
}

    var taro = Parson(name: "TARO", age: 25) // motherプロパティは初期化していないのでnilになっている
    var json:JSON = JSON.Parse(taro)

    textView.text = json.rawString() // 表示

f:id:furuya02:20150730040810p:plain

    var taro = Parson(name: "TARO", age: 25)
    taro.mother = Parson(name:"HANAKO",age:50) // motherプロパティにParsonをセットする
    var json:JSON = JSON.Parse(taro)
        
    textView.text = json.rawString() // 表示

f:id:furuya02:20150730040832p:plain

4 配列への対応

調子に乗って、配列への対応も挑戦してみます。
配列の検出は、クラス名から行いました。

extension JSON {
    static func Parse(data:Any) -> JSON{
        var json:JSON=[:]
        let ref = reflect(data)
        // 配列検出
        var str = _stdlib_getDemangledTypeName(data)
        if str.hasPrefix("Swift.Array") {
            if let range = str.rangeOfString(".") {
                
                // クラス名もこんな感じで取れるけど、とりあえず必要ないか・・・
                //let separators = NSCharacterSet(charactersInString: "<.>")
                //var tmp = str.componentsSeparatedByCharactersInSet(separators)
                //var className = tmp[tmp.count-2]
                
                var ar:[JSON] = []
                for var i = 0; i < ref.count; ++i {
                    let (key, mirror) = ref[i]
                    var j = Parse(mirror.value)
                    ar.append(j)
                }
                json = JSON(ar) // 配列としてJSONに格納する
            }
        }else{ // 以下は、先ほどと変化なし
            for var i = 0; i < ref.count; ++i {
                
                let (key, mirror) = ref[i]
                
                //nil検出(やや強引ですがAny型がnilと比較できなかったので・・・)
                if mirror.disposition == .Optional && "\(mirror.value)" == "nil" {                        json[key] = nil
                } else {
                    //とりあえず、String,Int,Boolのみ対応
                    if let n = mirror.value as? String {
                        json[key] = JSON(n)
                    } else if let n = mirror.value as? Int{
                        json[key] = JSON(n)
                    } else if let n = mirror.value as? Bool{
                        json[key] = JSON(n)
                    } else {
                        let subref = reflect(mirror.value)
                        let (k, m) = subref[0]
                        json[key] = Parse(m.value) // 再帰
                    }
                }
            }
        }
        return json
    }
}

Parsonクラスは、前回と同じです。

    var ar:[Parson] = [] 
    var taro = Parson(name: "TARO", age: 25) 
    taro.mother = Parson(name:"HANAKO",age:50)
    ar.append(taro)// 配列の1件目
    ar.append(Parson(name: "KEISUKE", age: 21))// 配列の2件目
    var json:JSON = JSON.Parse(ar)

    textView.text = json.rawString() // 表示

f:id:furuya02:20150730041936p:plain

5 まとめ

現時点で、Int、Bool、String以外、まったく対応できていません。
Enumは、挑戦してみたのですが、型が取れずに挫折しました。

かなり、力ずくで、かつ適当なので、実用には程遠いですが、僅かでもリフレクションを書く際の参考になれば幸いです。

6 参考にさせて頂いたリンク

SwiftyJSON/SwiftyJSON · GitHub
MirrorType - NSHipster
Simple Reflection in Swift
Swiftでリフレクションを使ってインスタンスからプロパティの一覧を取得する - 酢ろぐ!
Swift Reflection (Swift 的反射) - 猫·仁波切
Safx: Swiftでリフレクションを使ってみる
Swift1.2でクラス名が取得できなくなった - Qiita