Swiftで面倒なJSONの取り扱いをさらに10倍便利にするclass JSON

  • 483
    いいね
  • 3
    コメント
この記事は最終更新日から1年以上が経過しています。

Swiftで面倒なJSONの取り扱いを10倍便利にするSwiftyJSONをもってしても、まだJavaScriptよりも面倒だったのが悔しかったので書きました。

Synopsis

AnyObjectなSwiftオブジェクトも、こうして JSON Stringにできます。

let obj:[String:AnyObject] = [
    "array": [JSON.null, false, 0, "",[],[:]],
    "object":[
        "null":   JSON.null,
        "bool":   true,
        "int":    42,
        "double": 3.141592653589793,
        "string": "a α\t\n?",
        "array":  [],
        "object": [:]
    ],
    "url":"http://blog.livedoor.com/dankogai/"
]

let json = JSON(obj)
json.toString()
// "{\"array\":[null,false,0,\"\",[],{}],
//  \"object\":{\"int\":42,\"double\":3.141592653589793,
//  \"string\":\"a α\t弾\n?\",\"object\":{},\"null\":null,
//  \"bool\":true,\"array\":[]},
//  \"url\":\"http://blog.livedoor.com/dankogai/\"}"

もちろん文字列からも

let json = JSON.parse("{\"array\":[...}")

URLからも。

let json = JSON.fromURL("http://api.dan.co.jp/jsonenv")

JSONの歩き方。

添字で葉までいって、型をあわせるだけ。

json["object"]["null"].asNull       // NSNull()
json["object"]["bool"].asBool       // true
json["object"]["int"].asInt         // 42
json["object"]["double"].asDouble   // 3.141592653589793
json["object"]["string"].asString   // "a α\t弾\n?"

json["array"][0].asNull             // NSNull()
json["array"][1].asBool             // false
json["array"][2].asInt              // 0
json["array"][3].asString           // ""

SwiftyJSONと同様、存在しない添字にアクセスしても爆発しません。その代わり発生したエラーをほいほいと次のメソッドに投げていきます。Optional ChainならぬNSError Chainといったところでしょうか。

if let b = json["noexistent"][1234567890]["entry"].asBool {
    // ....
} else {
    let e = json["noexistent"][1234567890]["entry"].asError
    println(e)
} // Error Domain=JSONErrorDomain Code=404 "["noexistent"] not found" UserInfo=0x10064bfc0 {NSLocalizedDescription=["noexistent"] not found}

ここまではSwiftyJSONと同様なのですが、一つ困ったことがあったのです。

SwiftyJSONのJSONValueは、enumだったのです。

なんでenumだと困るのでしょう?

class JSONは継承可

ここでJavaScriptにおけるJSONの取り扱いを見てみましょう。

//json["object"]["string"] vs...
  json.object.string

はい、さらに短い。オブジェクトの一分岐ごとに4文字も。Swift(というか厳密にはNSDictionary)では添字を使わざるを得なかったところを、JavaScriptではプロパティで行けるからです。

でもプロパティの追加は、何もJavaScriptの専売特許ではありませんよね?

こうすればいけるではありませんか?

//// schema by subclassing
class MyJSON : JSON {
    override init(_ obj:AnyObject){ super.init(obj) }
    override init(_ json:JSON)  { super.init(json) }
    var null  :NSNull? { return self["null"].asNull }
    var bool  :Bool?   { return self["bool"].asBool }
    var int   :Int?    { return self["int"].asInt }
    var double:Double? { return self["double"].asDouble }
    var string:String? { return self["string"].asString }
    var url:   String? { return self["url"].asString }
    var array :MyJSON  { return MyJSON(self["array"])  }
    var object:MyJSON  { return MyJSON(self["object"]) }
}

行けました!

let myjson = MyJSON(obj)
myjson.object.null      // NSNull?
myjson.object.bool      // Bool?
myjson.object.int       // Int?
myjson.object.double    // Double?
myjson.object.string    // String?
myjson.url              // String?

JavaScriptではゼロ円でやってくれる連想配列のプロパティ化のために、一行メソッドの羅列とはいえclassを一個定義するのはめんどくさくも感じます。その一方、ソースコードできちんと定義しておくことじゃ、意図しないプロパティをtypoで作ってしまうことがありえないことも意味します。そもそもコンパイル通りませんし、IDEによる補完もこれで効くようになります。さらに各プロパティ(というかgetter)で入力検証したりするのもこれで自由自在です。

Swiftにはプリミティブでない型が三種類もあります。が、classには他の二つと二つの大きな違いがあります。一つは値の代入で、残りの二つが値渡しなのに対して参照渡しであること。そしてもう一つが、継承をサポートしていることです。

structenumにも、新たなプロパティを追加するのはextensionで可能です。が、これだとオールオアナッシングなんですよね。サイトごとにschemaを変えたいってのが出来ない。

この問題があるので、ArrayDictionaryにもclass版が欲しいところではありますね。

Dan the Man with too Much Data to (De)?serialize

追記[2016.01.29]: Swift 2にも対応しているのみならず、Playgroundも付いています。