JavaScript
JSON
Swift

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

More than 3 years have passed since last update.

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も付いています。